From 755ddfe9cdfbf8df05f404b66997d0629ec3db97 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Mon, 24 Apr 2023 14:18:52 +0200 Subject: [PATCH 01/13] [Enterprise Search] Copyedit Elasticsearch, Search Applications (#155604) Minor copy clean up for clarity and concision --- .../components/elasticsearch_guide/elasticsearch_guide.tsx | 4 ++-- .../components/engines/engines_list.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx index 3820f8e334f07..7da4392ef1112 100644 --- a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx @@ -79,7 +79,7 @@ export const ElasticsearchGuide: React.FC = () => { 'xpack.enterpriseSearch.overview.elasticsearchGuide.elasticsearchDescription', { defaultMessage: - 'Whether you are building a search-powered application, or designing a large-scale search implementation, Elasticsearch provides the low-level tools to create the most relevant and performant search experience.', + "Elasticsearch provides the low-level tools you need to build fast, relevant search for your website or application. Because it's powerful and flexible, Elasticsearch can handle search use cases of all shapes and sizes.", } )}

@@ -103,7 +103,7 @@ export const ElasticsearchGuide: React.FC = () => { 'xpack.enterpriseSearch.overview.elasticsearchGuide.connectToElasticsearchDescription', { defaultMessage: - "Elastic builds and maintains clients in several popular languages and our community has contributed many more. They're easy to work with, feel natural to use, and, just like Elasticsearch, don't limit what you might want to do with them.", + 'Elastic builds and maintains clients in several popular languages and our community has contributed many more.', } )}

diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx index 48dce9f524164..7b93214e0af8b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx @@ -166,7 +166,7 @@ export const EnginesList: React.FC = ({ createEngineFlyoutOpen }) => description: ( Date: Mon, 24 Apr 2023 08:20:44 -0400 Subject: [PATCH 02/13] [Synthetics] Improve toast information for add/update monitor (#155319) --- ...e_monitor_save.ts => use_monitor_save.tsx} | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) rename x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/{use_monitor_save.ts => use_monitor_save.tsx} (74%) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx similarity index 74% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx index 9fd18e5dfa04d..d8638c4b9ed92 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx @@ -6,8 +6,9 @@ */ import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public'; +import { toMountPoint, useKibana } from '@kbn/kibana-react-plugin/public'; import { useParams, useRouteMatch } from 'react-router-dom'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { MONITOR_EDIT_ROUTE } from '../../../../../../common/constants'; @@ -18,6 +19,8 @@ import { cleanMonitorListState } from '../../../state'; import { useSyntheticsRefreshContext } from '../../../contexts'; export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonitor }) => { + const core = useKibana(); + const theme$ = core.services.theme?.theme$; const dispatch = useDispatch(); const { refreshApp } = useSyntheticsRefreshContext(); const { monitorId } = useParams<{ monitorId: string }>(); @@ -51,10 +54,16 @@ export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonito dispatch(cleanMonitorListState()); kibanaService.toasts.addSuccess({ title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, + text: toMountPoint( +

+ {monitorId ? MONITOR_UPDATED_SUCCESS_LABEL_SUBTEXT : MONITOR_SUCCESS_LABEL_SUBTEXT} +

, + { theme$ } + ), toastLifeTimeMs: 3000, }); } - }, [data, status, monitorId, loading, refreshApp, dispatch]); + }, [data, status, monitorId, loading, refreshApp, dispatch, theme$]); return { status, loading, isEdit }; }; @@ -66,6 +75,13 @@ const MONITOR_SUCCESS_LABEL = i18n.translate( } ); +const MONITOR_SUCCESS_LABEL_SUBTEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage.subtext', + { + defaultMessage: 'It will next run according to its defined schedule.', + } +); + const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( 'xpack.synthetics.monitorManagement.monitorEditedSuccessMessage', { @@ -79,3 +95,10 @@ const MONITOR_FAILURE_LABEL = i18n.translate( defaultMessage: 'Monitor was unable to be saved. Please try again later.', } ); + +const MONITOR_UPDATED_SUCCESS_LABEL_SUBTEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorFailureMessage.subtext', + { + defaultMessage: 'It will next run according to its defined schedule.', + } +); From 9eee24f7bfc557d9aff48e30f3358d542ef5f476 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 24 Apr 2023 06:01:05 -0700 Subject: [PATCH 03/13] [Security Solution] Multi level grouping for alerts table (#152862) ## Multi Level Grouping Resolves https://github.com/elastic/kibana/issues/150516 Resolves https://github.com/elastic/kibana/issues/150514 Implements multi level grouping in Alerts table and Rule details table. Supports 3 levels deep. https://user-images.githubusercontent.com/6935300/232547389-7d778f69-d96d-4bd8-8560-f5ddd9fe8060.mov ### Test plan https://docs.google.com/document/d/15oseanNzF-u-Xeoahy1IVxI4oV3wOuO8VhA886cA1U8/edit# ### To do - [Cypress](https://github.com/elastic/kibana/issues/150666) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Steph Milovic --- .../steps/storybooks/build_and_upload.ts | 2 + .../.storybook/main.js | 9 + .../kbn-securitysolution-grouping/README.md | 3 - .../kbn-securitysolution-grouping/README.mdx | 3 + .../kbn-securitysolution-grouping/index.tsx | 8 +- .../accordion_panel/group_stats.test.tsx | 4 +- .../accordion_panel/group_stats.tsx | 31 +- .../components/accordion_panel/index.test.tsx | 1 + .../src/components/accordion_panel/index.tsx | 30 +- .../components/group_selector/index.test.tsx | 36 +- .../src/components/group_selector/index.tsx | 61 +- .../src/components/grouping.mock.tsx | 111 ++ .../src/components/grouping.stories.tsx | 28 + .../src/components/grouping.test.tsx | 95 +- .../src/components/grouping.tsx | 159 +- .../src/components/index.tsx | 5 +- .../src/components/styles.tsx | 37 +- .../src/components/translations.ts | 8 +- .../src/components/types.ts | 6 +- .../src/containers/query/index.ts | 2 +- .../src/containers/query/types.ts | 3 +- .../src/hooks/state/actions.ts | 53 +- .../src/hooks/state/reducer.test.ts | 39 +- .../src/hooks/state/reducer.ts | 48 +- .../src/hooks/types.ts | 32 +- .../src/hooks/use_get_group_selector.test.tsx | 115 +- .../src/hooks/use_get_group_selector.tsx | 116 +- .../src/hooks/use_grouping.test.tsx | 13 +- .../src/hooks/use_grouping.tsx | 80 +- .../src/hooks/use_grouping_pagination.ts | 53 - .../tsconfig.json | 3 +- src/dev/storybook/aliases.ts | 1 + .../alerts_treemap_panel/index.test.tsx | 11 - .../components/alerts_treemap_panel/index.tsx | 2 +- .../public/common/components/top_n/index.tsx | 2 +- .../alerts/use_alert_prevalence.test.ts | 46 - .../containers/alerts/use_alert_prevalence.ts | 2 +- .../containers/use_global_time/index.test.tsx | 74 +- .../containers/use_global_time/index.tsx | 22 +- .../public/common/store/grouping/actions.ts | 6 +- .../public/common/store/grouping/reducer.ts | 15 +- .../public/common/store/grouping/selectors.ts | 4 - .../public/common/store/grouping/types.ts | 1 - .../pages/rule_details/index.tsx | 2 +- .../alerts_count_panel/index.test.tsx | 13 - .../alerts_kpis/alerts_count_panel/index.tsx | 2 +- .../alerts_histogram_panel/index.tsx | 2 +- .../use_summary_chart_data.tsx | 2 +- .../alerts_table/alerts_grouping.test.tsx | 376 ++++ .../alerts_table/alerts_grouping.tsx | 341 ++-- .../alerts_table/alerts_sub_grouping.tsx | 259 +++ .../group_take_action_items.test.tsx | 119 +- .../group_take_action_items.tsx | 147 +- .../alerts_table/grouping_settings/mock.ts | 1736 +++++++++++++++++ .../grouping_settings/query_builder.ts | 6 +- .../components/alerts_table/index.test.tsx | 261 --- .../use_persistent_controls.tsx | 8 +- .../detection_engine.test.tsx | 159 +- .../detection_engine/detection_engine.tsx | 30 +- .../entity_analytics/anomalies/index.tsx | 2 +- .../entity_analytics/header/index.tsx | 2 +- 61 files changed, 3631 insertions(+), 1216 deletions(-) create mode 100644 packages/kbn-securitysolution-grouping/.storybook/main.js delete mode 100644 packages/kbn-securitysolution-grouping/README.md create mode 100644 packages/kbn-securitysolution-grouping/README.mdx create mode 100644 packages/kbn-securitysolution-grouping/src/components/grouping.mock.tsx create mode 100644 packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx delete mode 100644 packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts delete mode 100644 x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index 949cb0a0ff534..b16e75abdb8a1 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -15,6 +15,7 @@ const STORYBOOKS = [ 'apm', 'canvas', 'cases', + 'cell_actions', 'ci_composite', 'cloud_chat', 'coloring', @@ -34,6 +35,7 @@ const STORYBOOKS = [ 'expression_shape', 'expression_tagcloud', 'fleet', + 'grouping', 'home', 'infra', 'kibana_react', diff --git a/packages/kbn-securitysolution-grouping/.storybook/main.js b/packages/kbn-securitysolution-grouping/.storybook/main.js new file mode 100644 index 0000000000000..8dc3c5d1518f4 --- /dev/null +++ b/packages/kbn-securitysolution-grouping/.storybook/main.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/packages/kbn-securitysolution-grouping/README.md b/packages/kbn-securitysolution-grouping/README.md deleted file mode 100644 index 87b8047720a37..0000000000000 --- a/packages/kbn-securitysolution-grouping/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/securitysolution-grouping - -Grouping component and query. Currently only consumed by security solution alerts table. Package is a WIP. Refactoring to make generic https://github.com/elastic/kibana/issues/152491 diff --git a/packages/kbn-securitysolution-grouping/README.mdx b/packages/kbn-securitysolution-grouping/README.mdx new file mode 100644 index 0000000000000..b79cac381c298 --- /dev/null +++ b/packages/kbn-securitysolution-grouping/README.mdx @@ -0,0 +1,3 @@ +# @kbn/securitysolution-grouping + +Grouping component and query. Currently only consumed by security solution alerts table. diff --git a/packages/kbn-securitysolution-grouping/index.tsx b/packages/kbn-securitysolution-grouping/index.tsx index 92d69af316e1f..1b83c314714b7 100644 --- a/packages/kbn-securitysolution-grouping/index.tsx +++ b/packages/kbn-securitysolution-grouping/index.tsx @@ -6,20 +6,22 @@ * Side Public License, v 1. */ -import { RawBucket, StatRenderer, getGroupingQuery, isNoneGroup, useGrouping } from './src'; +import { getGroupingQuery, isNoneGroup, useGrouping } from './src'; import type { + DynamicGroupingProps, GroupOption, GroupingAggregation, - GroupingFieldTotalAggregation, NamedAggregation, + RawBucket, + StatRenderer, } from './src'; export { getGroupingQuery, isNoneGroup, useGrouping }; export type { + DynamicGroupingProps, GroupOption, GroupingAggregation, - GroupingFieldTotalAggregation, NamedAggregation, RawBucket, StatRenderer, diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx index 8df4c6ad6c7dc..8ccc1c912b62d 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx @@ -13,6 +13,8 @@ import { GroupStats } from './group_stats'; const onTakeActionsOpen = jest.fn(); const testProps = { bucketKey: '9nk5mo2fby', + groupFilter: [], + groupNumber: 0, onTakeActionsOpen, statRenderers: [ { @@ -23,7 +25,7 @@ const testProps = { { title: 'Rules:', badge: { value: 2 } }, { title: 'Alerts:', badge: { value: 2, width: 50, color: '#a83632' } }, ], - takeActionItems: [ + takeActionItems: () => [

,

, ], diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx index 00c6e7aa3a855..61f40982507b8 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx @@ -16,29 +16,44 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { Filter } from '@kbn/es-query'; import { StatRenderer } from '../types'; import { statsContainerCss } from '../styles'; import { TAKE_ACTION } from '../translations'; interface GroupStatsProps { bucketKey: string; - statRenderers?: StatRenderer[]; + groupFilter: Filter[]; + groupNumber: number; onTakeActionsOpen?: () => void; - takeActionItems: JSX.Element[]; + statRenderers?: StatRenderer[]; + takeActionItems: (groupFilters: Filter[], groupNumber: number) => JSX.Element[]; } const GroupStatsComponent = ({ bucketKey, - statRenderers, + groupFilter, + groupNumber, onTakeActionsOpen, - takeActionItems, + statRenderers, + takeActionItems: getTakeActionItems, }: GroupStatsProps) => { const [isPopoverOpen, setPopover] = useState(false); + const [takeActionItems, setTakeActionItems] = useState([]); - const onButtonClick = useCallback( - () => (!isPopoverOpen && onTakeActionsOpen ? onTakeActionsOpen() : setPopover(!isPopoverOpen)), - [isPopoverOpen, onTakeActionsOpen] - ); + const onButtonClick = useCallback(() => { + if (!isPopoverOpen && takeActionItems.length === 0) { + setTakeActionItems(getTakeActionItems(groupFilter, groupNumber)); + } + return !isPopoverOpen && onTakeActionsOpen ? onTakeActionsOpen() : setPopover(!isPopoverOpen); + }, [ + getTakeActionItems, + groupFilter, + groupNumber, + isPopoverOpen, + onTakeActionsOpen, + takeActionItems.length, + ]); const statsComponent = useMemo( () => diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx index 9aa4b51437130..828e1059471e9 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx @@ -55,6 +55,7 @@ const testProps = { }, renderChildComponent, selectedGroup: 'kibana.alert.rule.name', + onGroupClose: () => {}, }; describe('grouping accordion panel', () => { diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx index 286cb18ffb6e6..c1d55495cf785 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx @@ -8,7 +8,7 @@ import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { firstNonNullValue } from '../../helpers'; import type { RawBucket } from '../types'; import { createGroupFilter } from './helpers'; @@ -20,8 +20,9 @@ interface GroupPanelProps { forceState?: 'open' | 'closed'; groupBucket: RawBucket; groupPanelRenderer?: JSX.Element; + groupingLevel?: number; isLoading: boolean; - level?: number; + onGroupClose: () => void; onToggleGroup?: (isOpen: boolean, groupBucket: RawBucket) => void; renderChildComponent: (groupFilter: Filter[]) => React.ReactElement; selectedGroup: string; @@ -40,18 +41,30 @@ const DefaultGroupPanelRenderer = ({ title }: { title: string }) => ( ); const GroupPanelComponent = ({ - customAccordionButtonClassName = 'groupingAccordionForm__button', + customAccordionButtonClassName, customAccordionClassName = 'groupingAccordionForm', extraAction, forceState, groupBucket, groupPanelRenderer, + groupingLevel = 0, isLoading, - level = 0, + onGroupClose, onToggleGroup, renderChildComponent, selectedGroup, }: GroupPanelProps) => { + const lastForceState = useRef(forceState); + useEffect(() => { + if (lastForceState.current === 'open' && forceState === 'closed') { + // when parent group closes, reset pagination of any child groups + onGroupClose(); + lastForceState.current = 'closed'; + } else if (lastForceState.current === 'closed' && forceState === 'open') { + lastForceState.current = 'open'; + } + }, [onGroupClose, forceState, selectedGroup]); + const groupFieldValue = useMemo(() => firstNonNullValue(groupBucket.key), [groupBucket.key]); const groupFilters = useMemo( @@ -72,20 +85,21 @@ const GroupPanelComponent = ({ +

{groupPanelRenderer ?? }
} - className={customAccordionClassName} + buttonElement="div" + className={groupingLevel > 0 ? 'groupingAccordionFormLevel' : customAccordionClassName} data-test-subj="grouping-accordion" extraAction={extraAction} forceState={forceState} isLoading={isLoading} - id={`group${level}-${groupFieldValue}`} + id={`group${groupingLevel}-${groupFieldValue}`} onToggle={onToggle} paddingSize="m" > - {renderChildComponent(groupFilters)} + {renderChildComponent(groupFilters)} ); }; diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx index daa58396df70b..2172390f41e85 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx @@ -43,7 +43,7 @@ const testProps = { esTypes: ['ip'], }, ], - groupSelected: 'kibana.alert.rule.name', + groupsSelected: ['kibana.alert.rule.name'], onGroupChange, options: [ { @@ -90,4 +90,38 @@ describe('group selector', () => { fireEvent.click(getByTestId('panel-none')); expect(onGroupChange).toHaveBeenCalled(); }); + it('Labels button in correct selection order', () => { + const { getByTestId, rerender } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, User name, Host name'); + rerender( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name, User name'); + }); + it('Labels button with selection not in options', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name'); + }); + it('Labels button when `none` is selected', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name'); + }); }); diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx index f0274f7c73ab7..a2a876da97992 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx @@ -21,21 +21,27 @@ export interface GroupSelectorProps { 'data-test-subj'?: string; fields: FieldSpec[]; groupingId: string; - groupSelected: string; + groupsSelected: string[]; onGroupChange: (groupSelection: string) => void; options: Array<{ key: string; label: string }>; title?: string; + maxGroupingLevels?: number; } - const GroupSelectorComponent = ({ 'data-test-subj': dataTestSubj, fields, - groupSelected = 'none', + groupsSelected = ['none'], onGroupChange, options, title = i18n.GROUP_BY, + maxGroupingLevels = 1, }: GroupSelectorProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const isGroupSelected = useCallback( + (groupKey: string) => + !!groupsSelected.find((selectedGroupKey) => selectedGroupKey === groupKey), + [groupsSelected] + ); const panels: EuiContextMenuPanelDescriptor[] = useMemo( () => [ @@ -49,7 +55,7 @@ const GroupSelectorComponent = ({ style={{ lineHeight: 1 }} > - {i18n.SELECT_FIELD.toUpperCase()} + {i18n.SELECT_FIELD(maxGroupingLevels)} onGroupChange('none'), }, ...options.map((o) => ({ 'data-test-subj': `panel-${o.key}`, + disabled: groupsSelected.length === maxGroupingLevels && !isGroupSelected(o.key), name: o.label, onClick: () => onGroupChange(o.key), - icon: groupSelected === o.key ? 'check' : 'empty', + icon: isGroupSelected(o.key) ? 'check' : 'empty', })), { 'data-test-subj': `panel-custom`, name: i18n.CUSTOM_FIELD, icon: 'empty', + disabled: groupsSelected.length === maxGroupingLevels, panel: 'customPanel', + hasPanel: true, }, ], }, @@ -91,24 +100,35 @@ const GroupSelectorComponent = ({ currentOptions={options.map((o) => ({ text: o.label, field: o.key }))} onSubmit={(field: string) => { onGroupChange(field); + setIsPopoverOpen(false); }} fields={fields} /> ), }, ], - [fields, groupSelected, onGroupChange, options] + [fields, groupsSelected.length, isGroupSelected, maxGroupingLevels, onGroupChange, options] ); - const selectedOption = useMemo( - () => options.filter((groupOption) => groupOption.key === groupSelected), - [groupSelected, options] + const selectedOptions = useMemo( + () => options.filter((groupOption) => isGroupSelected(groupOption.key)), + [isGroupSelected, options] ); const onButtonClick = useCallback(() => setIsPopoverOpen((currentVal) => !currentVal), []); const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const button = useMemo( - () => ( + const button = useMemo(() => { + // need to use groupsSelected to ensure proper selection order (selectedOptions does not handle selection order) + const buttonLabel = isGroupSelected('none') + ? i18n.NONE + : groupsSelected.reduce((optionsTitle, o) => { + const selection = selectedOptions.find((opt) => opt.key === o); + if (selection == null) { + return optionsTitle; + } + return optionsTitle ? [optionsTitle, selection.label].join(', ') : selection.label; + }, ''); + return ( 0 - ? selectedOption[0].label - : i18n.NONE - } + title={buttonLabel} size="xs" > - {`${title}: ${ - groupSelected !== 'none' && selectedOption.length > 0 - ? selectedOption[0].label - : i18n.NONE - }`} + {`${title}: ${buttonLabel}`} - ), - [groupSelected, onButtonClick, selectedOption, title] - ); + ); + }, [groupsSelected, isGroupSelected, onButtonClick, selectedOptions, title]); return (

{'child component'}

, + onGroupClose: () => {}, + selectedGroup: 'kibana.alert.rule.name', + takeActionItems: () => [ + {}}> + {'Mark as acknowledged'} + , + {}}> + {'Mark as closed'} + , + ], + tracker: () => {}, +}; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx new file mode 100644 index 0000000000000..b961402ee3a7c --- /dev/null +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { mockGroupingProps } from './grouping.mock'; +import { Grouping } from './grouping'; +import readme from '../../README.mdx'; + +export default { + component: Grouping, + title: 'Grouping', + description: 'A group of accordion components that each renders a given child component', + parameters: { + docs: { + page: readme, + }, + }, +}; + +export const Emtpy: Story = () => { + return ; +}; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx index ff5a7d66a6042..2376614ab444c 100644 --- a/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx @@ -14,104 +14,15 @@ import { createGroupFilter } from './accordion_panel/helpers'; import { METRIC_TYPE } from '@kbn/analytics'; import { getTelemetryEvent } from '../telemetry/const'; +import { mockGroupingProps, rule1Name, rule2Name } from './grouping.mock'; + const renderChildComponent = jest.fn(); const takeActionItems = jest.fn(); const mockTracker = jest.fn(); -const rule1Name = 'Rule 1 name'; -const rule1Desc = 'Rule 1 description'; -const rule2Name = 'Rule 2 name'; -const rule2Desc = 'Rule 2 description'; const testProps = { - data: { - groupsCount: { - value: 2, - }, - groupByFields: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: [rule1Name, rule1Desc], - key_as_string: `${rule1Name}|${rule1Desc}`, - doc_count: 1, - hostsCountAggregation: { - value: 1, - }, - ruleTags: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - alertsCount: { - value: 1, - }, - severitiesSubAggregation: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'low', - doc_count: 1, - }, - ], - }, - countSeveritySubAggregation: { - value: 1, - }, - usersCountAggregation: { - value: 1, - }, - }, - { - key: [rule2Name, rule2Desc], - key_as_string: `${rule2Name}|${rule2Desc}`, - doc_count: 1, - hostsCountAggregation: { - value: 1, - }, - ruleTags: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - unitsCount: { - value: 1, - }, - severitiesSubAggregation: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'low', - doc_count: 1, - }, - ], - }, - countSeveritySubAggregation: { - value: 1, - }, - usersCountAggregation: { - value: 1, - }, - }, - ], - }, - unitsCount: { - value: 2, - }, - }, - groupingId: 'test-grouping-id', - isLoading: false, - pagination: { - pageIndex: 0, - pageSize: 25, - onChangeItemsPerPage: jest.fn(), - onChangePage: jest.fn(), - itemsPerPageOptions: [10, 25, 50, 100], - }, + ...mockGroupingProps, renderChildComponent, - selectedGroup: 'kibana.alert.rule.name', takeActionItems, tracker: mockTracker, }; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx index d77f1fe1a8106..625beda320d04 100644 --- a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx @@ -21,35 +21,29 @@ import { createGroupFilter } from './accordion_panel/helpers'; import { GroupPanel } from './accordion_panel'; import { GroupStats } from './accordion_panel/group_stats'; import { EmptyGroupingComponent } from './empty_results_panel'; -import { groupingContainerCss, countCss } from './styles'; +import { countCss, groupingContainerCss, groupingContainerCssLevel } from './styles'; import { GROUPS_UNIT } from './translations'; -import type { - GroupingAggregation, - GroupingFieldTotalAggregation, - GroupPanelRenderer, - RawBucket, -} from './types'; -import { getTelemetryEvent } from '../telemetry/const'; +import type { GroupingAggregation, GroupPanelRenderer } from './types'; import { GroupStatsRenderer, OnGroupToggle } from './types'; +import { getTelemetryEvent } from '../telemetry/const'; export interface GroupingProps { - data?: GroupingAggregation & GroupingFieldTotalAggregation; - groupingId: string; + activePage: number; + data?: GroupingAggregation; groupPanelRenderer?: GroupPanelRenderer; groupSelector?: JSX.Element; // list of custom UI components which correspond to your custom rendered metrics aggregations groupStatsRenderer?: GroupStatsRenderer; + groupingId: string; + groupingLevel?: number; inspectButton?: JSX.Element; isLoading: boolean; + itemsPerPage: number; + onChangeGroupsItemsPerPage?: (size: number) => void; + onChangeGroupsPage?: (index: number) => void; onGroupToggle?: OnGroupToggle; - pagination: { - pageIndex: number; - pageSize: number; - onChangeItemsPerPage: (itemsPerPageNumber: number) => void; - onChangePage: (pageNumber: number) => void; - itemsPerPageOptions: number[]; - }; renderChildComponent: (groupFilter: Filter[]) => React.ReactElement; + onGroupClose: () => void; selectedGroup: string; takeActionItems: (groupFilters: Filter[], groupNumber: number) => JSX.Element[]; tracker?: ( @@ -61,24 +55,29 @@ export interface GroupingProps { } const GroupingComponent = ({ + activePage, data, - groupingId, groupPanelRenderer, groupSelector, groupStatsRenderer, + groupingId, + groupingLevel = 0, inspectButton, isLoading, + itemsPerPage, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + onGroupClose, onGroupToggle, - pagination, renderChildComponent, selectedGroup, takeActionItems, tracker, unit = defaultUnit, }: GroupingProps) => { - const [trigger, setTrigger] = useState< - Record }> - >({}); + const [trigger, setTrigger] = useState>( + {} + ); const unitCount = data?.unitsCount?.value ?? 0; const unitCountText = useMemo(() => { @@ -100,16 +99,16 @@ const GroupingComponent = ({ return ( } forceState={(trigger[groupKey] && trigger[groupKey].state) ?? 'closed'} @@ -128,7 +127,6 @@ const GroupingComponent = ({ // ...trigger, -> this change will keep only one group at a time expanded and one table displayed [groupKey]: { state: isOpen ? 'open' : 'closed', - selectedBucket: groupBucket, }, }); onGroupToggle?.({ isOpen, groupName: group, groupNumber, groupingId }); @@ -139,8 +137,9 @@ const GroupingComponent = ({ : () => } selectedGroup={selectedGroup} + groupingLevel={groupingLevel} /> - + {groupingLevel > 0 ? null : } ); }), @@ -149,7 +148,9 @@ const GroupingComponent = ({ groupPanelRenderer, groupStatsRenderer, groupingId, + groupingLevel, isLoading, + onGroupClose, onGroupToggle, renderChildComponent, selectedGroup, @@ -159,58 +160,76 @@ const GroupingComponent = ({ ] ); const pageCount = useMemo( - () => (groupCount && pagination.pageSize ? Math.ceil(groupCount / pagination.pageSize) : 1), - [groupCount, pagination.pageSize] + () => (groupCount ? Math.ceil(groupCount / itemsPerPage) : 1), + [groupCount, itemsPerPage] ); + return ( <> - - - {groupCount > 0 && unitCount > 0 ? ( - - - - {unitCountText} - - - - - {groupCountText} - - + {groupingLevel > 0 ? null : ( + + + {groupCount > 0 && unitCount > 0 ? ( + + + + {unitCountText} + + + + + {groupCountText} + + + + ) : null} + + + + {inspectButton && {inspectButton}} + {groupSelector} - ) : null} - - - - {inspectButton && {inspectButton}} - {groupSelector} - - - -
+ + + )} +
0 ? groupingContainerCssLevel : groupingContainerCss} + className="eui-xScroll" + > {isLoading && ( )} {groupCount > 0 ? ( <> {groupPanels} - - + {groupCount > 0 && ( + <> + + { + if (onChangeGroupsItemsPerPage) { + onChangeGroupsItemsPerPage(pageSize); + } + }} + onChangePage={(pageIndex: number) => { + if (onChangeGroupsPage) { + onChangeGroupsPage(pageIndex); + } + }} + pageCount={pageCount} + showPerPageOptions + /> + + )} ) : ( diff --git a/packages/kbn-securitysolution-grouping/src/components/index.tsx b/packages/kbn-securitysolution-grouping/src/components/index.tsx index c924da988b04e..0d759c0be48be 100644 --- a/packages/kbn-securitysolution-grouping/src/components/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/index.tsx @@ -14,8 +14,9 @@ export * from './grouping'; /** * Checks if no group is selected - * @param groupKey selected group field value + * @param groupKeys selected group field values * * @returns {boolean} True if no group is selected */ -export const isNoneGroup = (groupKey: string | null) => groupKey === NONE_GROUP_KEY; +export const isNoneGroup = (groupKeys: string[]) => + !!groupKeys.find((groupKey) => groupKey === NONE_GROUP_KEY); diff --git a/packages/kbn-securitysolution-grouping/src/components/styles.tsx b/packages/kbn-securitysolution-grouping/src/components/styles.tsx index abf3d83db508e..fcd2e4a61aaee 100644 --- a/packages/kbn-securitysolution-grouping/src/components/styles.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/styles.tsx @@ -36,7 +36,7 @@ export const statsContainerCss = css` `; export const groupingContainerCss = css` - .euiAccordion__childWrapper .euiAccordion__padding--m { + .groupingAccordionForm .euiAccordion__childWrapper .euiAccordion__padding--m { margin-left: 8px; margin-right: 8px; border-left: ${euiThemeVars.euiBorderThin}; @@ -44,7 +44,7 @@ export const groupingContainerCss = css` border-bottom: ${euiThemeVars.euiBorderThin}; border-radius: 0 0 6px 6px; } - .euiAccordion__triggerWrapper { + .groupingAccordionForm .euiAccordion__triggerWrapper { border-bottom: ${euiThemeVars.euiBorderThin}; border-left: ${euiThemeVars.euiBorderThin}; border-right: ${euiThemeVars.euiBorderThin}; @@ -59,8 +59,37 @@ export const groupingContainerCss = css` border-radius: 6px; min-width: 1090px; } - .groupingAccordionForm__button { - text-decoration: none !important; + .groupingPanelRenderer { + display: table; + table-layout: fixed; + width: 100%; + padding-right: 32px; + } +`; + +export const groupingContainerCssLevel = css` + .groupingAccordionFormLevel .euiAccordion__childWrapper .euiAccordion__padding--m { + margin-left: 8px; + margin-right: 8px; + border-left: none; + border-right: none; + border-bottom: ${euiThemeVars.euiBorderThin}; + border-radius: 0; + } + .groupingAccordionFormLevel .euiAccordion__triggerWrapper { + border-bottom: ${euiThemeVars.euiBorderThin}; + border-left: none; + border-right: none; + min-height: 78px; + padding-left: 16px; + padding-right: 16px; + border-radius: 0; + } + .groupingAccordionFormLevel { + border-top: none; + border-bottom: none; + border-radius: 0; + min-width: 1090px; } .groupingPanelRenderer { display: table; diff --git a/packages/kbn-securitysolution-grouping/src/components/translations.ts b/packages/kbn-securitysolution-grouping/src/components/translations.ts index e3896d25b910f..f2cb8a172dbdc 100644 --- a/packages/kbn-securitysolution-grouping/src/components/translations.ts +++ b/packages/kbn-securitysolution-grouping/src/components/translations.ts @@ -35,9 +35,11 @@ export const GROUP_BY_CUSTOM_FIELD = i18n.translate('grouping.customGroupByPanel defaultMessage: 'Group By Custom Field', }); -export const SELECT_FIELD = i18n.translate('grouping.groupByPanelTitle', { - defaultMessage: 'Select Field', -}); +export const SELECT_FIELD = (groupingLevelsCount: number) => + i18n.translate('grouping.groupByPanelTitle', { + values: { groupingLevelsCount }, + defaultMessage: 'Select up to {groupingLevelsCount} groupings', + }); export const NONE = i18n.translate('grouping.noneGroupByOptionName', { defaultMessage: 'None', diff --git a/packages/kbn-securitysolution-grouping/src/components/types.ts b/packages/kbn-securitysolution-grouping/src/components/types.ts index 8956056581cc6..cf5f55f5c27f3 100644 --- a/packages/kbn-securitysolution-grouping/src/components/types.ts +++ b/packages/kbn-securitysolution-grouping/src/components/types.ts @@ -19,7 +19,7 @@ export type RawBucket = GenericBuckets & T; /** Defines the shape of the aggregation returned by Elasticsearch */ // TODO: write developer docs for these fields -export interface GroupingAggregation { +export interface RootAggregation { groupByFields?: { buckets?: Array>; }; @@ -39,6 +39,8 @@ export type GroupingFieldTotalAggregation = Record< } >; +export type GroupingAggregation = RootAggregation & GroupingFieldTotalAggregation; + export interface BadgeMetric { value: number; color?: string; @@ -67,3 +69,5 @@ export type OnGroupToggle = (params: { groupNumber: number; groupingId: string; }) => void; + +export type { GroupingProps } from './grouping'; diff --git a/packages/kbn-securitysolution-grouping/src/containers/query/index.ts b/packages/kbn-securitysolution-grouping/src/containers/query/index.ts index 986788bf0dfa0..23699c1ccf94a 100644 --- a/packages/kbn-securitysolution-grouping/src/containers/query/index.ts +++ b/packages/kbn-securitysolution-grouping/src/containers/query/index.ts @@ -35,10 +35,10 @@ export const getGroupingQuery = ({ additionalFilters = [], from, groupByFields, - pageNumber, rootAggregations, runtimeMappings, size = DEFAULT_GROUP_BY_FIELD_SIZE, + pageNumber, sort, statsAggregations, to, diff --git a/packages/kbn-securitysolution-grouping/src/containers/query/types.ts b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts index c56e26223550a..5a8b2f822fb5c 100644 --- a/packages/kbn-securitysolution-grouping/src/containers/query/types.ts +++ b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts @@ -24,9 +24,10 @@ export interface GroupingQueryArgs { additionalFilters: BoolAgg[]; from: string; groupByFields: string[]; - pageNumber?: number; rootAggregations?: NamedAggregation[]; runtimeMappings?: MappingRuntimeFields; + additionalAggregationsRoot?: NamedAggregation[]; + pageNumber?: number; size?: number; sort?: Array<{ [category: string]: { order: 'asc' | 'desc' } }>; statsAggregations?: NamedAggregation[]; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts index 0953e6872ee91..e1bdb08500fa8 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts @@ -6,55 +6,20 @@ * Side Public License, v 1. */ -import { - ActionType, - GroupOption, - UpdateActiveGroup, - UpdateGroupActivePage, - UpdateGroupItemsPerPage, - UpdateGroupOptions, -} from '../types'; +import { ActionType, GroupOption, UpdateActiveGroups, UpdateGroupOptions } from '../types'; -const updateActiveGroup = ({ - activeGroup, +const updateActiveGroups = ({ + activeGroups, id, }: { - activeGroup: string; + activeGroups: string[]; id: string; -}): UpdateActiveGroup => ({ +}): UpdateActiveGroups => ({ payload: { - activeGroup, + activeGroups, id, }, - type: ActionType.updateActiveGroup, -}); - -const updateGroupActivePage = ({ - activePage, - id, -}: { - activePage: number; - id: string; -}): UpdateGroupActivePage => ({ - payload: { - activePage, - id, - }, - type: ActionType.updateGroupActivePage, -}); - -const updateGroupItemsPerPage = ({ - itemsPerPage, - id, -}: { - itemsPerPage: number; - id: string; -}): UpdateGroupItemsPerPage => ({ - payload: { - itemsPerPage, - id, - }, - type: ActionType.updateGroupItemsPerPage, + type: ActionType.updateActiveGroups, }); const updateGroupOptions = ({ @@ -72,8 +37,6 @@ const updateGroupOptions = ({ }); export const groupActions = { - updateActiveGroup, - updateGroupActivePage, - updateGroupItemsPerPage, + updateActiveGroups, updateGroupOptions, }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts index 5348731d39128..5a1b4112df3aa 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts @@ -24,7 +24,7 @@ const groupById = { [groupingId]: { ...defaultGroup, options: groupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; @@ -54,7 +54,7 @@ describe('grouping reducer', () => { JSON.stringify(groupingState.groupById) ); }); - it('updateActiveGroup', () => { + it('updateActiveGroups', () => { const { result } = renderHook(() => useReducer(groupsReducerWithStorage, { ...initialState, @@ -62,40 +62,11 @@ describe('grouping reducer', () => { }) ); let [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activeGroup).toEqual('host.name'); + expect(groupingState.groupById[groupingId].activeGroups).toEqual(['host.name']); act(() => { - dispatch(groupActions.updateActiveGroup({ id: groupingId, activeGroup: 'user.name' })); + dispatch(groupActions.updateActiveGroups({ id: groupingId, activeGroups: ['user.name'] })); }); [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activeGroup).toEqual('user.name'); - }); - it('updateGroupActivePage', () => { - const { result } = renderHook(() => - useReducer(groupsReducerWithStorage, { - ...initialState, - groupById, - }) - ); - let [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activePage).toEqual(0); - act(() => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: 12 })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activePage).toEqual(12); - }); - it('updateGroupItemsPerPage', () => { - const { result } = renderHook(() => useReducer(groupsReducerWithStorage, initialState)); - let [groupingState, dispatch] = result.current; - act(() => { - dispatch(groupActions.updateGroupOptions({ id: groupingId, newOptionList: groupingOptions })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].itemsPerPage).toEqual(25); - act(() => { - dispatch(groupActions.updateGroupItemsPerPage({ id: groupingId, itemsPerPage: 12 })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].itemsPerPage).toEqual(12); + expect(groupingState.groupById[groupingId].activeGroups).toEqual(['user.name']); }); }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts index 287227b5763b3..d59637b69defe 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts @@ -25,8 +25,8 @@ export const initialState: GroupMap = { const groupsReducer = (state: GroupMap, action: Action, groupsById: GroupsById) => { switch (action.type) { - case ActionType.updateActiveGroup: { - const { id, activeGroup } = action.payload; + case ActionType.updateActiveGroups: { + const { id, activeGroups } = action.payload; return { ...state, groupById: { @@ -34,35 +34,7 @@ const groupsReducer = (state: GroupMap, action: Action, groupsById: GroupsById) [id]: { ...defaultGroup, ...groupsById[id], - activeGroup, - }, - }, - }; - } - case ActionType.updateGroupActivePage: { - const { id, activePage } = action.payload; - return { - ...state, - groupById: { - ...groupsById, - [id]: { - ...defaultGroup, - ...groupsById[id], - activePage, - }, - }, - }; - } - case ActionType.updateGroupItemsPerPage: { - const { id, itemsPerPage } = action.payload; - return { - ...state, - groupById: { - ...groupsById, - [id]: { - ...defaultGroup, - ...groupsById[id], - itemsPerPage, + activeGroups, }, }, }; @@ -89,22 +61,10 @@ export const groupsReducerWithStorage = (state: GroupMap, action: Action) => { if (storage) { groupsInStorage = getAllGroupsInStorage(storage); } - const trackedGroupIds = Object.keys(state.groupById); - - const adjustedStorageGroups = Object.entries(groupsInStorage).reduce( - (acc: GroupsById, [key, group]) => ({ - ...acc, - [key]: { - // reset page to 0 if is initial state - ...(trackedGroupIds.includes(key) ? group : { ...group, activePage: 0 }), - }, - }), - {} as GroupsById - ); const groupsById: GroupsById = { ...state.groupById, - ...adjustedStorageGroups, + ...groupsInStorage, }; const newState = groupsReducer(state, action, groupsById); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/types.ts b/packages/kbn-securitysolution-grouping/src/hooks/types.ts index 4b5480794b30d..5c3e85d211eaf 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/types.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/types.ts @@ -8,35 +8,21 @@ // action types export enum ActionType { - updateActiveGroup = 'UPDATE_ACTIVE_GROUP', - updateGroupActivePage = 'UPDATE_GROUP_ACTIVE_PAGE', - updateGroupItemsPerPage = 'UPDATE_GROUP_ITEMS_PER_PAGE', + updateActiveGroups = 'UPDATE_ACTIVE_GROUPS', updateGroupOptions = 'UPDATE_GROUP_OPTIONS', } -export interface UpdateActiveGroup { - type: ActionType.updateActiveGroup; - payload: { activeGroup: string; id: string }; +export interface UpdateActiveGroups { + type: ActionType.updateActiveGroups; + payload: { activeGroups: string[]; id: string }; } -export interface UpdateGroupActivePage { - type: ActionType.updateGroupActivePage; - payload: { activePage: number; id: string }; -} -export interface UpdateGroupItemsPerPage { - type: ActionType.updateGroupItemsPerPage; - payload: { itemsPerPage: number; id: string }; -} export interface UpdateGroupOptions { type: ActionType.updateGroupOptions; payload: { newOptionList: GroupOption[]; id: string }; } -export type Action = - | UpdateActiveGroup - | UpdateGroupActivePage - | UpdateGroupItemsPerPage - | UpdateGroupOptions; +export type Action = UpdateActiveGroups | UpdateGroupOptions; // state @@ -46,10 +32,8 @@ export interface GroupOption { } export interface GroupModel { - activeGroup: string; + activeGroups: string[]; options: GroupOption[]; - activePage: number; - itemsPerPage: number; } export interface GroupsById { @@ -73,8 +57,6 @@ export interface Storage { export const EMPTY_GROUP_BY_ID: GroupsById = {}; export const defaultGroup: GroupModel = { - activePage: 0, - itemsPerPage: 25, - activeGroup: 'none', + activeGroups: ['none'], options: [], }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx index d29313b36e518..d741e7d15e670 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx @@ -52,7 +52,7 @@ describe('useGetGroupSelector', () => { useGetGroupSelector({ ...defaultArgs, groupingState: { - groupById: { [groupingId]: { ...defaultGroup, activeGroup: customField } }, + groupById: { [groupingId]: { ...defaultGroup, activeGroups: [customField] } }, }, }) ); @@ -72,12 +72,12 @@ describe('useGetGroupSelector', () => { }); }); - it('On group change, does nothing when set to prev selected group', () => { + it('On group change, removes selected group if already selected', () => { const testGroup = { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -89,15 +89,22 @@ describe('useGetGroupSelector', () => { }, }); act(() => result.current.props.onGroupChange('host.name')); - expect(dispatch).toHaveBeenCalledTimes(0); + + expect(dispatch).toHaveBeenCalledWith({ + payload: { + id: groupingId, + activeGroups: ['none'], + }, + type: ActionType.updateActiveGroups, + }); }); - it('On group change, resets active page, sets active group, and leaves options alone', () => { + it('On group change to none, remove all previously selected groups', () => { const testGroup = { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name', 'user.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -108,22 +115,43 @@ describe('useGetGroupSelector', () => { }, }, }); - act(() => result.current.props.onGroupChange('user.name')); - expect(dispatch).toHaveBeenNthCalledWith(1, { + act(() => result.current.props.onGroupChange('none')); + + expect(dispatch).toHaveBeenCalledWith({ payload: { id: groupingId, - activePage: 0, + activeGroups: ['none'], }, - type: ActionType.updateGroupActivePage, + type: ActionType.updateActiveGroups, }); - expect(dispatch).toHaveBeenNthCalledWith(2, { + }); + + it('On group change, resets active page, sets active group, and leaves options alone', () => { + const testGroup = { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name'], + }, + }; + const { result } = renderHook((props) => useGetGroupSelector(props), { + initialProps: { + ...defaultArgs, + groupingState: { + groupById: testGroup, + }, + }, + }); + act(() => result.current.props.onGroupChange('user.name')); + + expect(dispatch).toHaveBeenNthCalledWith(1, { payload: { id: groupingId, - activeGroup: 'user.name', + activeGroups: ['host.name', 'user.name'], }, - type: ActionType.updateActiveGroup, + type: ActionType.updateActiveGroups, }); - expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenCalledTimes(1); }); it('On group change, sends telemetry', () => { @@ -131,7 +159,7 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -155,7 +183,7 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -179,10 +207,10 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; - const { result } = renderHook((props) => useGetGroupSelector(props), { + const { result, rerender } = renderHook((props) => useGetGroupSelector(props), { initialProps: { ...defaultArgs, groupingState: { @@ -191,17 +219,54 @@ describe('useGetGroupSelector', () => { }, }); act(() => result.current.props.onGroupChange(customField)); - expect(dispatch).toHaveBeenCalledTimes(3); - expect(dispatch).toHaveBeenNthCalledWith(3, { + expect(dispatch).toHaveBeenCalledTimes(1); + rerender({ + ...defaultArgs, + groupingState: { + groupById: { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name', customField], + }, + }, + }, + }); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(2, { + payload: { + newOptionList: [...defaultGroupingOptions, { label: customField, key: customField }], + id: 'test-table', + }, + type: ActionType.updateGroupOptions, + }); + }); + + it('Supports multiple custom fields on initial load', () => { + const testGroup = { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name', customField, 'another.custom'], + }, + }; + renderHook((props) => useGetGroupSelector(props), { + initialProps: { + ...defaultArgs, + groupingState: { + groupById: testGroup, + }, + }, + }); + expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledWith({ payload: { - id: groupingId, newOptionList: [ ...defaultGroupingOptions, - { - label: customField, - key: customField, - }, + { label: customField, key: customField }, + { label: 'another.custom', key: 'another.custom' }, ], + id: 'test-table', }, type: ActionType.updateGroupOptions, }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx index e3b1c45b2733d..05920beb37a47 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx @@ -22,6 +22,7 @@ export interface UseGetGroupSelectorArgs { fields: FieldSpec[]; groupingId: string; groupingState: GroupMap; + maxGroupingLevels?: number; onGroupChange?: (param: { groupByField: string; tableId: string }) => void; tracker?: ( type: UiCounterMetricType, @@ -36,22 +37,21 @@ export const useGetGroupSelector = ({ fields, groupingId, groupingState, + maxGroupingLevels = 1, onGroupChange, tracker, }: UseGetGroupSelectorArgs) => { - const { activeGroup: selectedGroup, options } = + const { activeGroups: selectedGroups, options } = groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup; - const setGroupsActivePage = useCallback( - (activePage: number) => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage })); - }, - [dispatch, groupingId] - ); - - const setSelectedGroup = useCallback( - (activeGroup: string) => { - dispatch(groupActions.updateActiveGroup({ id: groupingId, activeGroup })); + const setSelectedGroups = useCallback( + (activeGroups: string[]) => { + dispatch( + groupActions.updateActiveGroups({ + id: groupingId, + activeGroups, + }) + ); }, [dispatch, groupingId] ); @@ -65,11 +65,20 @@ export const useGetGroupSelector = ({ const onChange = useCallback( (groupSelection: string) => { - if (groupSelection === selectedGroup) { + if (selectedGroups.find((selected) => selected === groupSelection)) { + const groups = selectedGroups.filter((selectedGroup) => selectedGroup !== groupSelection); + if (groups.length === 0) { + setSelectedGroups(['none']); + } else { + setSelectedGroups(groups); + } return; } - setGroupsActivePage(0); - setSelectedGroup(groupSelection); + + const newSelectedGroups = isNoneGroup([groupSelection]) + ? [groupSelection] + : [...selectedGroups.filter((selectedGroup) => selectedGroup !== 'none'), groupSelection]; + setSelectedGroups(newSelectedGroups); // built-in telemetry: UI-counter tracker?.( @@ -78,62 +87,57 @@ export const useGetGroupSelector = ({ ); onGroupChange?.({ tableId: groupingId, groupByField: groupSelection }); - - // only update options if the new selection is a custom field - if ( - !isNoneGroup(groupSelection) && - !options.find((o: GroupOption) => o.key === groupSelection) - ) { - setOptions([ - ...defaultGroupingOptions, - { - label: groupSelection, - key: groupSelection, - }, - ]); - } }, - [ - defaultGroupingOptions, - groupingId, - onGroupChange, - options, - selectedGroup, - setGroupsActivePage, - setOptions, - setSelectedGroup, - tracker, - ] + [groupingId, onGroupChange, selectedGroups, setSelectedGroups, tracker] ); useEffect(() => { - // only set options the first time, all other updates will be taken care of by onGroupChange - if (options.length > 0) return; - setOptions( - defaultGroupingOptions.find((o) => o.key === selectedGroup) - ? defaultGroupingOptions - : [ - ...defaultGroupingOptions, - ...(!isNoneGroup(selectedGroup) - ? [ - { + if (options.length === 0) { + return setOptions( + defaultGroupingOptions.find((o) => selectedGroups.find((selected) => selected === o.key)) + ? defaultGroupingOptions + : [ + ...defaultGroupingOptions, + ...(!isNoneGroup(selectedGroups) + ? selectedGroups.map((selectedGroup) => ({ key: selectedGroup, label: selectedGroup, - }, - ] - : []), - ] - ); - }, [defaultGroupingOptions, options.length, selectedGroup, setOptions]); + })) + : []), + ] + ); + } + if (isNoneGroup(selectedGroups)) { + return; + } + + const currentOptionKeys = options.map((o) => o.key); + const newOptions = [...options]; + selectedGroups.forEach((groupSelection) => { + if (currentOptionKeys.includes(groupSelection)) { + return; + } + // these are custom fields + newOptions.push({ + label: groupSelection, + key: groupSelection, + }); + }); + + if (newOptions.length !== options.length) { + setOptions(newOptions); + } + }, [defaultGroupingOptions, options, selectedGroups, setOptions]); return ( diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx index a2a9eeec8bf20..d95ae866d2ee9 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx @@ -30,7 +30,6 @@ const defaultArgs = { groupStatsRenderer: jest.fn(), inspectButton: <>, onGroupToggle: jest.fn(), - renderChildComponent: () =>

{'hello'}

, }, }; @@ -38,6 +37,9 @@ const groupingArgs = { data: {}, isLoading: false, takeActionItems: jest.fn(), + activePage: 0, + itemsPerPage: 25, + onGroupClose: () => {}, }; describe('useGrouping', () => { @@ -70,6 +72,8 @@ describe('useGrouping', () => { value: 18, }, }, + renderChildComponent: () =>

{'hello'}

, + selectedGroup: 'none', })} ); @@ -84,7 +88,7 @@ describe('useGrouping', () => { getItem.mockReturnValue( JSON.stringify({ 'test-table': { - activePage: 0, + itemsPerPageOptions: [10, 25, 50, 100], itemsPerPage: 25, activeGroup: 'kibana.alert.rule.name', options: defaultGroupingOptions, @@ -95,7 +99,7 @@ describe('useGrouping', () => { const { result, waitForNextUpdate } = renderHook(() => useGrouping(defaultArgs)); await waitForNextUpdate(); await waitForNextUpdate(); - const { getByTestId, queryByTestId } = render( + const { getByTestId } = render( {result.current.getGrouping({ ...groupingArgs, @@ -119,12 +123,13 @@ describe('useGrouping', () => { value: 18, }, }, + renderChildComponent: jest.fn(), + selectedGroup: 'test', })} ); expect(getByTestId('grouping-table')).toBeInTheDocument(); - expect(queryByTestId('innerTable')).not.toBeInTheDocument(); }); }); }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx index 993809943252f..5833ae8205d59 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx @@ -11,8 +11,7 @@ import React, { useCallback, useMemo, useReducer } from 'react'; import { UiCounterMetricType } from '@kbn/analytics'; import { groupsReducerWithStorage, initialState } from './state/reducer'; import { GroupingProps, GroupSelectorProps, isNoneGroup } from '..'; -import { useGroupingPagination } from './use_grouping_pagination'; -import { groupActions, groupByIdSelector } from './state'; +import { groupByIdSelector } from './state'; import { useGetGroupSelector } from './use_get_group_selector'; import { defaultGroup, GroupOption } from './types'; import { Grouping as GroupingComponent } from '../components/grouping'; @@ -23,33 +22,37 @@ import { Grouping as GroupingComponent } from '../components/grouping'; interface Grouping { getGrouping: (props: DynamicGroupingProps) => React.ReactElement; groupSelector: React.ReactElement; - pagination: { - reset: () => void; - pageIndex: number; - pageSize: number; - }; - selectedGroup: string; + selectedGroups: string[]; } -/** Type for static grouping component props where T is the `GroupingAggregation` +/** Type for static grouping component props where T is the consumer `GroupingAggregation` * @interface StaticGroupingProps */ type StaticGroupingProps = Pick< GroupingProps, - | 'groupPanelRenderer' - | 'groupStatsRenderer' - | 'inspectButton' - | 'onGroupToggle' - | 'renderChildComponent' - | 'unit' + 'groupPanelRenderer' | 'groupStatsRenderer' | 'onGroupToggle' | 'unit' >; -/** Type for dynamic grouping component props where T is the `GroupingAggregation` +/** Type for dynamic grouping component props where T is the consumer `GroupingAggregation` * @interface DynamicGroupingProps */ -type DynamicGroupingProps = Pick, 'data' | 'isLoading' | 'takeActionItems'>; +export type DynamicGroupingProps = Pick< + GroupingProps, + | 'activePage' + | 'data' + | 'groupingLevel' + | 'inspectButton' + | 'isLoading' + | 'itemsPerPage' + | 'onChangeGroupsItemsPerPage' + | 'onChangeGroupsPage' + | 'renderChildComponent' + | 'onGroupClose' + | 'selectedGroup' + | 'takeActionItems' +>; -/** Interface for configuring grouping package where T is the `GroupingAggregation` +/** Interface for configuring grouping package where T is the consumer `GroupingAggregation` * @interface GroupingArgs */ interface GroupingArgs { @@ -57,6 +60,7 @@ interface GroupingArgs { defaultGroupingOptions: GroupOption[]; fields: FieldSpec[]; groupingId: string; + maxGroupingLevels?: number; /** for tracking * @param param { groupByField: string; tableId: string } selected group and table id */ @@ -75,21 +79,22 @@ interface GroupingArgs { * @param defaultGroupingOptions defines the grouping options as an array of {@link GroupOption} * @param fields FieldSpec array serialized version of DataViewField fields. Available in the custom grouping options * @param groupingId Unique identifier of the grouping component. Used in local storage + * @param maxGroupingLevels maximum group nesting levels (optional) * @param onGroupChange callback executed when selected group is changed, used for tracking * @param tracker telemetry handler - * @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroup } + * @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroups } */ export const useGrouping = ({ componentProps, defaultGroupingOptions, fields, groupingId, + maxGroupingLevels, onGroupChange, tracker, }: GroupingArgs): Grouping => { const [groupingState, dispatch] = useReducer(groupsReducerWithStorage, initialState); - - const { activeGroup: selectedGroup } = useMemo( + const { activeGroups: selectedGroups } = useMemo( () => groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup, [groupingId, groupingState] ); @@ -100,56 +105,37 @@ export const useGrouping = ({ fields, groupingId, groupingState, + maxGroupingLevels, onGroupChange, tracker, }); - const pagination = useGroupingPagination({ groupingId, groupingState, dispatch }); - const getGrouping = useCallback( /** * * @param props {@link DynamicGroupingProps} */ (props: DynamicGroupingProps): React.ReactElement => - isNoneGroup(selectedGroup) ? ( - componentProps.renderChildComponent([]) + isNoneGroup([props.selectedGroup]) ? ( + props.renderChildComponent([]) ) : ( ), - [componentProps, groupSelector, groupingId, pagination, selectedGroup, tracker] + [componentProps, groupSelector, groupingId, tracker] ); - const resetPagination = useCallback(() => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: 0 })); - }, [groupingId]); - return useMemo( () => ({ getGrouping, groupSelector, - selectedGroup, - pagination: { - reset: resetPagination, - pageIndex: pagination.pageIndex, - pageSize: pagination.pageSize, - }, + selectedGroups, }), - [ - getGrouping, - groupSelector, - pagination.pageIndex, - pagination.pageSize, - resetPagination, - selectedGroup, - ] + [getGrouping, groupSelector, selectedGroups] ); }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts deleted file mode 100644 index 9aa07458aaf6f..0000000000000 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { useCallback, useMemo } from 'react'; -import { groupActions, groupByIdSelector } from './state'; -import { Action, defaultGroup, GroupMap } from './types'; - -export interface UseGroupingPaginationArgs { - dispatch: React.Dispatch; - groupingId: string; - groupingState: GroupMap; -} - -export const useGroupingPagination = ({ - groupingId, - groupingState, - dispatch, -}: UseGroupingPaginationArgs) => { - const { activePage, itemsPerPage } = - groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup; - - const setGroupsActivePage = useCallback( - (newActivePage: number) => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: newActivePage })); - }, - [dispatch, groupingId] - ); - - const setGroupsItemsPerPage = useCallback( - (newItemsPerPage: number) => { - dispatch( - groupActions.updateGroupItemsPerPage({ id: groupingId, itemsPerPage: newItemsPerPage }) - ); - }, - [dispatch, groupingId] - ); - - return useMemo( - () => ({ - pageIndex: activePage, - pageSize: itemsPerPage, - onChangeItemsPerPage: setGroupsItemsPerPage, - onChangePage: setGroupsActivePage, - itemsPerPageOptions: [10, 25, 50, 100], - }), - [activePage, itemsPerPage, setGroupsActivePage, setGroupsItemsPerPage] - ); -}; diff --git a/packages/kbn-securitysolution-grouping/tsconfig.json b/packages/kbn-securitysolution-grouping/tsconfig.json index ab98ec47e3c93..621ba68957cf1 100644 --- a/packages/kbn-securitysolution-grouping/tsconfig.json +++ b/packages/kbn-securitysolution-grouping/tsconfig.json @@ -6,7 +6,8 @@ "jest", "node", "react", - "@emotion/react/types/css-prop" + "@emotion/react/types/css-prop", + "@kbn/ambient-ui-types" ] }, "include": [ diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index da762f8f55725..c612ef05d4b7a 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -37,6 +37,7 @@ export const storybookAliases = { expression_shape: 'src/plugins/expression_shape/.storybook', expression_tagcloud: 'src/plugins/chart_expressions/expression_tagcloud/.storybook', fleet: 'x-pack/plugins/fleet/.storybook', + grouping: 'packages/kbn-securitysolution-grouping/.storybook', home: 'src/plugins/home/.storybook', infra: 'x-pack/plugins/infra/.storybook', kibana_react: 'src/plugins/kibana_react/.storybook', diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx index 5a3f4b3e25e0e..07342c4f60691 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { SecurityPageName } from '../../../../common/constants'; -import { useGlobalTime } from '../../containers/use_global_time'; import { DEFAULT_STACK_BY_FIELD, DEFAULT_STACK_BY_FIELD1, @@ -151,16 +150,6 @@ describe('AlertsTreemapPanel', () => { await waitFor(() => expect(screen.getByTestId('treemapPanel')).toBeInTheDocument()); }); - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', async () => { - render( - - - - ); - - await waitFor(() => expect(useGlobalTime).toBeCalledWith(false)); - }); - it('renders the panel with a hidden overflow-x', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx index a3ba582b3c974..33e526932c3e6 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx @@ -80,7 +80,7 @@ const AlertsTreemapPanelComponent: React.FC = ({ stackByWidth, title, }: Props) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${ALERTS_TREEMAP_ID}-${uuidv4()}`, []); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index e1b5f31f2d3b5..0113e0e584b54 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -110,7 +110,7 @@ const StatefulTopNComponent: React.FC = ({ value, }) => { const { uiSettings } = useKibana().services; - const { from, deleteQuery, setQuery, to } = useGlobalTime(false); + const { from, deleteQuery, setQuery, to } = useGlobalTime(); const options = getOptions(isActiveTimeline(scopeId ?? '') ? activeTimelineEventType : undefined); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts deleted file mode 100644 index b1a21d9ae492d..0000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { renderHook } from '@testing-library/react-hooks'; - -import { TestProviders } from '../../mock'; -import { useAlertPrevalence } from './use_alert_prevalence'; -import { useGlobalTime } from '../use_global_time'; - -const from = '2022-07-28T08:20:18.966Z'; -const to = '2022-07-28T08:20:18.966Z'; -jest.mock('../use_global_time', () => { - const actual = jest.requireActual('../use_global_time'); - return { - ...actual, - useGlobalTime: jest - .fn() - .mockReturnValue({ from, to, setQuery: jest.fn(), deleteQuery: jest.fn() }), - }; -}); - -describe('useAlertPrevalence', () => { - beforeEach(() => jest.resetAllMocks()); - - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', () => { - renderHook( - () => - useAlertPrevalence({ - field: 'host.name', - value: ['Host-byc3w6qlpo'], - isActiveTimelines: false, - signalIndexName: null, - includeAlertIds: false, - }), - { - wrapper: TestProviders, - } - ); - - expect(useGlobalTime).toBeCalledWith(false); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts index 03ac3d6169351..3e98e067bfe2d 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts +++ b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts @@ -44,7 +44,7 @@ export const useAlertPrevalence = ({ const timelineTime = useDeepEqualSelector((state) => inputsSelectors.timelineTimeRangeSelector(state) ); - const globalTime = useGlobalTime(false); + const globalTime = useGlobalTime(); let to: string | undefined; let from: string | undefined; if (ignoreTimerange === false) { diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx index 480ecdb3674ff..46a2738d6247a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx @@ -37,23 +37,77 @@ describe('useGlobalTime', () => { expect(result1.to).toBe(0); }); - test('clear all queries at unmount when clearAllQuery is set to true', () => { - const { unmount } = renderHook(() => useGlobalTime()); + test('clear query at unmount when setQuery has been called', () => { + const { result, unmount } = renderHook(() => useGlobalTime()); + act(() => { + result.current.setQuery({ + id: 'query-2', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + unmount(); - expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/local/inputs/DELETE_ALL_QUERY' + expect(mockDispatch.mock.calls.length).toBe(2); + expect(mockDispatch.mock.calls[1][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' ); }); - test('do NOT clear all queries at unmount when clearAllQuery is set to false.', () => { - const { unmount } = renderHook(() => useGlobalTime(false)); + test('do NOT clear query at unmount when setQuery has not been called', () => { + const { unmount } = renderHook(() => useGlobalTime()); unmount(); expect(mockDispatch.mock.calls.length).toBe(0); }); - test('do NOT clear all queries when setting state and clearAllQuery is set to true', () => { - const { rerender } = renderHook(() => useGlobalTime()); - act(() => rerender()); - expect(mockDispatch.mock.calls.length).toBe(0); + test('do clears only the dismounted queries at unmount when setQuery is called', () => { + const { result, unmount } = renderHook(() => useGlobalTime()); + + act(() => { + result.current.setQuery({ + id: 'query-1', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + + act(() => { + result.current.setQuery({ + id: 'query-2', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + + const { result: theOneWillNotBeDismounted } = renderHook(() => useGlobalTime()); + + act(() => { + theOneWillNotBeDismounted.current.setQuery({ + id: 'query-3h', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + unmount(); + expect(mockDispatch).toHaveBeenCalledTimes(5); + expect(mockDispatch.mock.calls[3][0].payload.id).toEqual('query-1'); + + expect(mockDispatch.mock.calls[3][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' + ); + + expect(mockDispatch.mock.calls[4][0].payload.id).toEqual('query-2'); + + expect(mockDispatch.mock.calls[4][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx index dbb57d57c3e6e..76cd23c8efba0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx @@ -6,7 +6,7 @@ */ import { pick } from 'lodash/fp'; -import { useCallback, useState, useEffect, useMemo } from 'react'; +import { useCallback, useState, useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { InputsModelId } from '../../store/inputs/constants'; @@ -15,15 +15,18 @@ import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; import type { SetQuery, DeleteQuery } from './types'; -export const useGlobalTime = (clearAllQuery: boolean = true) => { +export const useGlobalTime = () => { const dispatch = useDispatch(); const { from, to } = useDeepEqualSelector((state) => pick(['from', 'to'], inputsSelectors.globalTimeRangeSelector(state)) ); const [isInitializing, setIsInitializing] = useState(true); + const queryId = useRef([]); + const setQuery = useCallback( - ({ id, inspect, loading, refetch, searchSessionId }: SetQuery) => + ({ id, inspect, loading, refetch, searchSessionId }: SetQuery) => { + queryId.current = [...queryId.current, id]; dispatch( inputsActions.setQuery({ inputId: InputsModelId.global, @@ -33,7 +36,8 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { refetch, searchSessionId, }) - ), + ); + }, [dispatch] ); @@ -50,13 +54,13 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { // This effect must not have any mutable dependencies. Otherwise, the cleanup function gets called before the component unmounts. useEffect(() => { return () => { - if (clearAllQuery) { - dispatch(inputsActions.deleteAllQuery({ id: InputsModelId.global })); + if (queryId.current.length > 0) { + queryId.current.forEach((id) => deleteQuery({ id })); } }; - }, [dispatch, clearAllQuery]); + }, [deleteQuery]); - const memoizedReturn = useMemo( + return useMemo( () => ({ isInitializing, from, @@ -66,8 +70,6 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { }), [deleteQuery, from, isInitializing, setQuery, to] ); - - return memoizedReturn; }; export type GlobalTimeArgs = Omit, 'deleteQuery'> & diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts b/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts index a61186aeb0f8f..d78be8b03cb4d 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts @@ -11,9 +11,5 @@ import type React from 'react'; const actionCreator = actionCreatorFactory('x-pack/security_solution/groups'); export const updateGroupSelector = actionCreator<{ - groupSelector: React.ReactElement; + groupSelector: React.ReactElement | null; }>('UPDATE_GROUP_SELECTOR'); - -export const updateSelectedGroup = actionCreator<{ - selectedGroup: string; -}>('UPDATE_SELECTED_GROUP'); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts b/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts index aaea793e4ca86..6914e4ad465fe 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts @@ -6,20 +6,17 @@ */ import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { updateGroupSelector, updateSelectedGroup } from './actions'; +import { updateGroupSelector } from './actions'; import type { GroupModel } from './types'; export const initialGroupingState: GroupModel = { groupSelector: null, - selectedGroup: null, }; -export const groupsReducer = reducerWithInitialState(initialGroupingState) - .case(updateSelectedGroup, (state, { selectedGroup }) => ({ - ...state, - selectedGroup, - })) - .case(updateGroupSelector, (state, { groupSelector }) => ({ +export const groupsReducer = reducerWithInitialState(initialGroupingState).case( + updateGroupSelector, + (state, { groupSelector }) => ({ ...state, groupSelector, - })); + }) +); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts b/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts index eb63e256a4d9f..126fdac8c1b36 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts @@ -11,7 +11,3 @@ import type { GroupState } from './types'; const groupSelector = (state: GroupState) => state.groups.groupSelector; export const getGroupSelector = () => createSelector(groupSelector, (selector) => selector); - -export const selectedGroup = (state: GroupState) => state.groups.selectedGroup; - -export const getSelectedGroup = () => createSelector(selectedGroup, (group) => group); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/types.ts b/x-pack/plugins/security_solution/public/common/store/grouping/types.ts index 7d8fd4bc3eeca..d2250b15722ed 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/types.ts @@ -7,7 +7,6 @@ export interface GroupModel { groupSelector: React.ReactElement | null; - selectedGroup: string | null; } export interface GroupState { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 6e1b4fddbd167..90f1d38f69774 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -852,7 +852,7 @@ const RuleDetailsPageComponent: React.FC = ({ {ruleId != null && ( { }); }); - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', async () => { - await act(async () => { - mount( - - - - ); - - expect(useGlobalTime).toBeCalledWith(false); - }); - }); - it('renders with the specified `alignHeader` alignment', async () => { await act(async () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 26eb4522ed617..f9967a44ffb6e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -84,7 +84,7 @@ export const AlertsCountPanel = memo( isExpanded, setIsExpanded, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled'); // create a unique, but stable (across re-renders) query id diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index d1cb85cdf4564..e1945ca151cd8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -152,7 +152,7 @@ export const AlertsHistogramPanel = memo( isExpanded, setIsExpanded, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuidv4()}`, []); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx index fb5024d3c2e50..e8d0ddd061e81 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx @@ -82,7 +82,7 @@ export const useSummaryChartData: UseAlerts = ({ signalIndexName, skip = false, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const [updatedAt, setUpdatedAt] = useState(Date.now()); const [items, setItems] = useState([]); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx new file mode 100644 index 0000000000000..4a57d7cac8e73 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx @@ -0,0 +1,376 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, render, within } from '@testing-library/react'; +import type { Filter } from '@kbn/es-query'; +import useResizeObserver from 'use-resize-observer/polyfilled'; + +import '../../../common/mock/match_media'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../../common/mock'; +import type { AlertsTableComponentProps } from './alerts_grouping'; +import { GroupedAlertsTable } from './alerts_grouping'; +import { TableId } from '@kbn/securitysolution-data-table'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; +import { createStore } from '../../../common/store'; +import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__'; +import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { groupingSearchResponse } from './grouping_settings/mock'; + +jest.mock('../../containers/detection_engine/alerts/use_query'); +jest.mock('../../../common/containers/sourcerer'); +jest.mock('../../../common/utils/normalize_time_range'); +jest.mock('../../../common/containers/use_global_time', () => ({ + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), +})); + +const mockOptions = [ + { label: 'ruleName', key: 'kibana.alert.rule.name' }, + { label: 'userName', key: 'user.name' }, + { label: 'hostName', key: 'host.name' }, + { label: 'sourceIP', key: 'source.ip' }, +]; +// +jest.mock('./grouping_settings', () => { + const actual = jest.requireActual('./grouping_settings'); + + return { + ...actual, + getDefaultGroupingOptions: () => mockOptions, + }; +}); + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const mockUseFieldBrowserOptions = jest.fn(); +jest.mock('../../../timelines/components/fields_browser', () => ({ + useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), +})); + +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer/polyfilled'); +mockUseResizeObserver.mockImplementation(() => ({})); +const mockedUseKibana = mockUseKibana(); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../common/lib/kibana'); + + return { + ...original, + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + telemetry: mockedTelemetry, + }, + }), + }; +}); + +jest.mock('./timeline_actions/use_add_bulk_to_timeline', () => ({ + useAddBulkToTimelineAction: jest.fn(() => {}), +})); +const sourcererDataView = { + indicesExist: true, + loading: false, + indexPattern: { + fields: [], + }, + browserFields: {}, +}; +const renderChildComponent = (groupingFilters: Filter[]) =>

; + +const testProps: AlertsTableComponentProps = { + defaultFilters: [], + from: '2020-07-07T08:20:18.966Z', + globalFilters: [], + globalQuery: { + query: 'query', + language: 'language', + }, + hasIndexMaintenance: true, + hasIndexWrite: true, + loading: false, + renderChildComponent, + runtimeMappings: {}, + signalIndexName: 'test', + tableId: TableId.test, + to: '2020-07-08T08:20:18.966Z', +}; + +const mockUseQueryAlerts = useQueryAlerts as jest.Mock; +const mockQueryResponse = { + loading: false, + data: {}, + setQuery: () => {}, + response: '', + request: '', + refetch: () => {}, +}; + +const getMockStorageState = (groups: string[] = ['none']) => + JSON.stringify({ + [testProps.tableId]: { + activeGroups: groups, + options: mockOptions, + }, + }); + +describe('GroupedAlertsTable', () => { + const { storage } = createSecuritySolutionStorageMock(); + let store: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + selectedPatterns: ['myFakebeat-*'], + }); + mockUseQueryAlerts.mockImplementation((i) => { + if (i.skip) { + return mockQueryResponse; + } + if (i.query.aggs.groupByFields.multi_terms != null) { + return { + ...mockQueryResponse, + data: groupingSearchResponse.ruleName, + }; + } + return { + ...mockQueryResponse, + data: i.query.aggs.groupByFields.terms.field != null ? groupingSearchResponse.hostName : {}, + }; + }); + }); + + it('calls the proper initial dispatch actions for groups', () => { + const { getByTestId, queryByTestId } = render( + + + + ); + + expect(queryByTestId('empty-results-panel')).not.toBeInTheDocument(); + expect(queryByTestId('group-selector-dropdown')).not.toBeInTheDocument(); + expect(getByTestId('alerts-table')).toBeInTheDocument(); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch.mock.calls[0][0].type).toEqual( + 'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR' + ); + }); + + it('renders empty grouping table when group is selected without data', async () => { + mockUseQueryAlerts.mockReturnValue(mockQueryResponse); + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name'])); + const { getByTestId, queryByTestId } = render( + + + + ); + expect(queryByTestId('alerts-table')).not.toBeInTheDocument(); + expect(getByTestId('empty-results-panel')).toBeInTheDocument(); + }); + + it('renders grouping table in first accordion level when single group is selected', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name'])); + + const { getAllByTestId } = render( + + + + ); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + expect(within(level0).getByTestId('alerts-table')).toBeInTheDocument(); + }); + + it('renders grouping table in second accordion level when 2 groups are selected', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name'])); + + const { getAllByTestId } = render( + + + + ); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + expect(within(level0).queryByTestId('alerts-table')).not.toBeInTheDocument(); + + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + const level1 = within(getAllByTestId('grouping-accordion-content')[1]); + expect(level1.getByTestId('alerts-table')).toBeInTheDocument(); + }); + + it('resets all levels pagination when selected group changes', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + getByTestId('grouping-level-2-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual(null); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual('true'); + }); + + fireEvent.click(getAllByTestId('group-selector-dropdown')[0]); + fireEvent.click(getAllByTestId('panel-user.name')[0]); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + // level 2 has been removed with the group selection change + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual('true'); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual(null); + }); + }); + + it('resets all levels pagination when global query updates', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId, rerender } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + rerender( + + + + ); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + getByTestId('grouping-level-2-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual('true'); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual(null); + }); + }); + + it('resets only most inner group pagination when its parent groups open/close', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[28]); + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual(null); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual('true'); + }); + + expect( + within(getByTestId('grouping-level-2-pagination')) + .getByTestId('pagination-button-0') + .getAttribute('aria-current') + ).toEqual('true'); + expect( + within(getByTestId('grouping-level-2-pagination')) + .getByTestId('pagination-button-1') + .getAttribute('aria-current') + ).toEqual(null); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx index e5868970f6768..99ae3f4ff3433 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx @@ -5,50 +5,29 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; -import { useDispatch } from 'react-redux'; -import { v4 as uuidv4 } from 'uuid'; +import { useDispatch, useSelector } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; -import { buildEsQuery } from '@kbn/es-query'; -import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import type { - GroupingFieldTotalAggregation, - GroupingAggregation, -} from '@kbn/securitysolution-grouping'; -import { useGrouping, isNoneGroup } from '@kbn/securitysolution-grouping'; +import type { GroupOption } from '@kbn/securitysolution-grouping'; +import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; +import { isEmpty, isEqual } from 'lodash/fp'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; -import type { AlertsGroupingAggregation } from './grouping_settings/types'; +import { groupSelectors } from '../../../common/store/grouping'; +import type { State } from '../../../common/store'; +import { updateGroupSelector } from '../../../common/store/grouping/actions'; import type { Status } from '../../../../common/detection_engine/schemas/common'; -import { InspectButton } from '../../../common/components/inspect'; import { defaultUnit } from '../../../common/components/toolbar/unit'; -import { useGlobalTime } from '../../../common/containers/use_global_time'; -import { combineQueries } from '../../../common/lib/kuery'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; -import { useKibana } from '../../../common/lib/kibana'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { useInspectButton } from '../alerts_kpis/common/hooks'; - -import { buildTimeRangeFilter } from './helpers'; -import * as i18n from './translations'; -import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; -import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; -import { - getAlertsGroupingQuery, - getDefaultGroupingOptions, - renderGroupPanel, - getStats, - useGroupTakeActionsItems, -} from './grouping_settings'; -import { updateGroupSelector, updateSelectedGroup } from '../../../common/store/grouping/actions'; +import { getDefaultGroupingOptions, renderGroupPanel, getStats } from './grouping_settings'; +import { useKibana } from '../../../common/lib/kibana'; +import { GroupedSubLevel } from './alerts_sub_grouping'; import { track } from '../../../common/lib/telemetry'; -const ALERTS_GROUPING_ID = 'alerts-grouping'; - export interface AlertsTableComponentProps { - currentAlertStatusFilterValue?: Status; + currentAlertStatusFilterValue?: Status[]; defaultFilters?: Filter[]; from: string; globalFilters: Filter[]; @@ -63,52 +42,37 @@ export interface AlertsTableComponentProps { to: string; } -export const GroupedAlertsTableComponent: React.FC = ({ - defaultFilters = [], - from, - globalFilters, - globalQuery, - hasIndexMaintenance, - hasIndexWrite, - loading, - tableId, - to, - runtimeMappings, - signalIndexName, - currentAlertStatusFilterValue, - renderChildComponent, -}) => { - const dispatch = useDispatch(); +const DEFAULT_PAGE_SIZE = 25; +const DEFAULT_PAGE_INDEX = 0; +const MAX_GROUPING_LEVELS = 3; - const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( - SourcererScopeName.detections +const useStorage = (storage: Storage, tableId: string) => + useMemo( + () => ({ + getStoragePageSize: (): number[] => { + const pageSizes = storage.get(`grouping-table-${tableId}`); + if (!pageSizes) { + return Array(MAX_GROUPING_LEVELS).fill(DEFAULT_PAGE_SIZE); + } + return pageSizes; + }, + setStoragePageSize: (pageSizes: number[]) => { + storage.set(`grouping-table-${tableId}`, pageSizes); + }, + }), + [storage, tableId] ); + +const GroupedAlertsTableComponent: React.FC = (props) => { + const dispatch = useDispatch(); + + const { indexPattern, selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); + const { - services: { uiSettings, telemetry }, + services: { storage, telemetry }, } = useKibana(); - const getGlobalQuery = useCallback( - (customFilters: Filter[]) => { - if (browserFields != null && indexPattern != null) { - return combineQueries({ - config: getEsQueryConfig(uiSettings), - dataProviders: [], - indexPattern, - browserFields, - filters: [ - ...(defaultFilters ?? []), - ...globalFilters, - ...customFilters, - ...buildTimeRangeFilter(from, to), - ], - kqlQuery: globalQuery, - kqlMode: globalQuery.language, - }); - } - return null; - }, - [browserFields, indexPattern, uiSettings, defaultFilters, globalFilters, from, to, globalQuery] - ); + const { getStoragePageSize, setStoragePageSize } = useStorage(storage, props.tableId); const { onGroupChange, onGroupToggle } = useMemo( () => ({ @@ -125,153 +89,146 @@ export const GroupedAlertsTableComponent: React.FC = [telemetry] ); - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []); - - const inspect = useMemo( - () => ( - - ), - [uniqueQueryId] - ); - - const { groupSelector, getGrouping, selectedGroup, pagination } = useGrouping({ + const { groupSelector, getGrouping, selectedGroups } = useGrouping({ componentProps: { groupPanelRenderer: renderGroupPanel, groupStatsRenderer: getStats, - inspectButton: inspect, onGroupToggle, - renderChildComponent, unit: defaultUnit, }, - defaultGroupingOptions: getDefaultGroupingOptions(tableId), + defaultGroupingOptions: getDefaultGroupingOptions(props.tableId), fields: indexPattern.fields, - groupingId: tableId, + groupingId: props.tableId, + maxGroupingLevels: MAX_GROUPING_LEVELS, onGroupChange, tracker: track, }); - const resetPagination = pagination.reset; - useEffect(() => { - dispatch(updateGroupSelector({ groupSelector })); - }, [dispatch, groupSelector]); + const getGroupSelector = groupSelectors.getGroupSelector(); - useEffect(() => { - dispatch(updateSelectedGroup({ selectedGroup })); - }, [dispatch, selectedGroup]); - - useInvalidFilterQuery({ - id: tableId, - filterQuery: getGlobalQuery([])?.filterQuery, - kqlError: getGlobalQuery([])?.kqlError, - query: globalQuery, - startDate: from, - endDate: to, - }); + const groupSelectorInRedux = useSelector((state: State) => getGroupSelector(state)); + const selectorOptions = useRef([]); - const { deleteQuery, setQuery } = useGlobalTime(false); - const additionalFilters = useMemo(() => { - resetPagination(); - try { - return [ - buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [ - ...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []), - ...(defaultFilters ?? []), - ]), - ]; - } catch (e) { - return []; + useEffect(() => { + if ( + isNoneGroup(selectedGroups) && + groupSelector.props.options.length > 0 && + (groupSelectorInRedux == null || + !isEqual(selectorOptions.current, groupSelector.props.options)) + ) { + selectorOptions.current = groupSelector.props.options; + dispatch(updateGroupSelector({ groupSelector })); + } else if (!isNoneGroup(selectedGroups) && groupSelectorInRedux !== null) { + dispatch(updateGroupSelector({ groupSelector: null })); } - }, [defaultFilters, globalFilters, globalQuery, resetPagination]); + }, [dispatch, groupSelector, groupSelectorInRedux, selectedGroups]); - const queryGroups = useMemo( - () => - getAlertsGroupingQuery({ - additionalFilters, - selectedGroup, - from, - runtimeMappings, - to, - pageSize: pagination.pageSize, - pageIndex: pagination.pageIndex, - }), - [ - additionalFilters, - selectedGroup, - from, - runtimeMappings, - to, - pagination.pageSize, - pagination.pageIndex, - ] + const [pageIndex, setPageIndex] = useState( + Array(MAX_GROUPING_LEVELS).fill(DEFAULT_PAGE_INDEX) ); + const [pageSize, setPageSize] = useState(getStoragePageSize); - const { - data: alertsGroupsData, - loading: isLoadingGroups, - refetch, - request, - response, - setQuery: setAlertsQuery, - } = useQueryAlerts< - {}, - GroupingAggregation & - GroupingFieldTotalAggregation - >({ - query: queryGroups, - indexName: signalIndexName, - queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING, - skip: isNoneGroup(selectedGroup), - }); + const resetAllPagination = useCallback(() => { + setPageIndex((curr) => curr.map(() => DEFAULT_PAGE_INDEX)); + }, []); useEffect(() => { - if (!isNoneGroup(selectedGroup)) { - setAlertsQuery(queryGroups); - } - }, [queryGroups, selectedGroup, setAlertsQuery]); + resetAllPagination(); + }, [resetAllPagination, selectedGroups]); + + const setPageVar = useCallback( + (newNumber: number, groupingLevel: number, pageType: 'index' | 'size') => { + if (pageType === 'index') { + setPageIndex((currentIndex) => { + const newArr = [...currentIndex]; + newArr[groupingLevel] = newNumber; + return newArr; + }); + } - useInspectButton({ - deleteQuery, - loading: isLoadingGroups, - response, - setQuery, - refetch, - request, - uniqueQueryId, - }); + if (pageType === 'size') { + setPageSize((currentIndex) => { + const newArr = [...currentIndex]; + newArr[groupingLevel] = newNumber; + setStoragePageSize(newArr); + return newArr; + }); + } + }, + [setStoragePageSize] + ); - const takeActionItems = useGroupTakeActionsItems({ - indexName: indexPattern.title, - currentStatus: currentAlertStatusFilterValue, - showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, + const nonGroupingFilters = useRef({ + defaultFilters: props.defaultFilters, + globalFilters: props.globalFilters, + globalQuery: props.globalQuery, }); - const getTakeActionItems = useCallback( - (groupFilters: Filter[], groupNumber: number) => - takeActionItems({ - query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, - tableId, - groupNumber, - selectedGroup, - }), - [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] - ); + useEffect(() => { + const nonGrouping = { + defaultFilters: props.defaultFilters, + globalFilters: props.globalFilters, + globalQuery: props.globalQuery, + }; + if (!isEqual(nonGroupingFilters.current, nonGrouping)) { + resetAllPagination(); + nonGroupingFilters.current = nonGrouping; + } + }, [props.defaultFilters, props.globalFilters, props.globalQuery, resetAllPagination]); + + const getLevel = useCallback( + (level: number, selectedGroup: string, parentGroupingFilter?: string) => { + let rcc; + if (level < selectedGroups.length - 1) { + rcc = (groupingFilters: Filter[]) => { + return getLevel( + level + 1, + selectedGroups[level + 1], + JSON.stringify([ + ...groupingFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]) + ); + }; + } else { + rcc = (groupingFilters: Filter[]) => { + return props.renderChildComponent([ + ...groupingFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]); + }; + } - const groupedAlerts = useMemo( - () => - getGrouping({ - data: alertsGroupsData?.aggregations, - isLoading: loading || isLoadingGroups, - takeActionItems: getTakeActionItems, - }), - [alertsGroupsData?.aggregations, getGrouping, getTakeActionItems, isLoadingGroups, loading] + const resetGroupChildrenPagination = (parentLevel: number) => { + setPageIndex((allPages) => { + const resetPages = allPages.splice(parentLevel + 1, allPages.length); + return [...allPages, ...resetPages.map(() => DEFAULT_PAGE_INDEX)]; + }); + }; + return ( + resetGroupChildrenPagination(level)} + pageIndex={pageIndex[level] ?? DEFAULT_PAGE_INDEX} + pageSize={pageSize[level] ?? DEFAULT_PAGE_SIZE} + parentGroupingFilter={parentGroupingFilter} + renderChildComponent={rcc} + selectedGroup={selectedGroup} + setPageIndex={(newIndex: number) => setPageVar(newIndex, level, 'index')} + setPageSize={(newSize: number) => setPageVar(newSize, level, 'size')} + /> + ); + }, + [getGrouping, pageIndex, pageSize, props, selectedGroups, setPageVar] ); if (isEmpty(selectedPatterns)) { return null; } - return groupedAlerts; + return getLevel(0, selectedGroups[0]); }; export const GroupedAlertsTable = React.memo(GroupedAlertsTableComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx new file mode 100644 index 0000000000000..cf8a8128cb166 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx @@ -0,0 +1,259 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import type { Filter, Query } from '@kbn/es-query'; +import { buildEsQuery } from '@kbn/es-query'; +import type { GroupingAggregation } from '@kbn/securitysolution-grouping'; +import { isNoneGroup } from '@kbn/securitysolution-grouping'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import type { DynamicGroupingProps } from '@kbn/securitysolution-grouping/src'; +import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; +import { combineQueries } from '../../../common/lib/kuery'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import type { AlertsGroupingAggregation } from './grouping_settings/types'; +import type { Status } from '../../../../common/detection_engine/schemas/common'; +import { InspectButton } from '../../../common/components/inspect'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import { useKibana } from '../../../common/lib/kibana'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; +import { useInspectButton } from '../alerts_kpis/common/hooks'; +import { buildTimeRangeFilter } from './helpers'; + +import * as i18n from './translations'; +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; +import { getAlertsGroupingQuery, useGroupTakeActionsItems } from './grouping_settings'; + +const ALERTS_GROUPING_ID = 'alerts-grouping'; + +interface OwnProps { + currentAlertStatusFilterValue?: Status[]; + defaultFilters?: Filter[]; + from: string; + getGrouping: ( + props: Omit, 'groupSelector' | 'pagination'> + ) => React.ReactElement; + globalFilters: Filter[]; + globalQuery: Query; + groupingLevel?: number; + hasIndexMaintenance: boolean; + hasIndexWrite: boolean; + loading: boolean; + onGroupClose: () => void; + pageIndex: number; + pageSize: number; + parentGroupingFilter?: string; + renderChildComponent: (groupingFilters: Filter[]) => React.ReactElement; + runtimeMappings: MappingRuntimeFields; + selectedGroup: string; + setPageIndex: (newIndex: number) => void; + setPageSize: (newSize: number) => void; + signalIndexName: string | null; + tableId: TableIdLiteral; + to: string; +} + +export type AlertsTableComponentProps = OwnProps; + +export const GroupedSubLevelComponent: React.FC = ({ + currentAlertStatusFilterValue, + defaultFilters = [], + from, + getGrouping, + globalFilters, + globalQuery, + groupingLevel, + hasIndexMaintenance, + hasIndexWrite, + loading, + onGroupClose, + pageIndex, + pageSize, + parentGroupingFilter, + renderChildComponent, + runtimeMappings, + selectedGroup, + setPageIndex, + setPageSize, + signalIndexName, + tableId, + to, +}) => { + const { + services: { uiSettings }, + } = useKibana(); + const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.detections); + + const getGlobalQuery = useCallback( + (customFilters: Filter[]) => { + if (browserFields != null && indexPattern != null) { + return combineQueries({ + config: getEsQueryConfig(uiSettings), + dataProviders: [], + indexPattern, + browserFields, + filters: [ + ...(defaultFilters ?? []), + ...globalFilters, + ...customFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ...buildTimeRangeFilter(from, to), + ], + kqlQuery: globalQuery, + kqlMode: globalQuery.language, + }); + } + return null; + }, + [ + browserFields, + defaultFilters, + from, + globalFilters, + globalQuery, + indexPattern, + parentGroupingFilter, + to, + uiSettings, + ] + ); + + const additionalFilters = useMemo(() => { + try { + return [ + buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [ + ...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []), + ...(defaultFilters ?? []), + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]), + ]; + } catch (e) { + return []; + } + }, [defaultFilters, globalFilters, globalQuery, parentGroupingFilter]); + + const queryGroups = useMemo(() => { + return getAlertsGroupingQuery({ + additionalFilters, + selectedGroup, + from, + runtimeMappings, + to, + pageSize, + pageIndex, + }); + }, [additionalFilters, from, pageIndex, pageSize, runtimeMappings, selectedGroup, to]); + + const emptyGlobalQuery = useMemo(() => getGlobalQuery([]), [getGlobalQuery]); + + useInvalidFilterQuery({ + id: tableId, + filterQuery: emptyGlobalQuery?.filterQuery, + kqlError: emptyGlobalQuery?.kqlError, + query: globalQuery, + startDate: from, + endDate: to, + }); + + const { + data: alertsGroupsData, + loading: isLoadingGroups, + refetch, + request, + response, + setQuery: setAlertsQuery, + } = useQueryAlerts<{}, GroupingAggregation>({ + query: queryGroups, + indexName: signalIndexName, + queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING, + skip: isNoneGroup([selectedGroup]), + }); + + useEffect(() => { + if (!isNoneGroup([selectedGroup])) { + setAlertsQuery(queryGroups); + } + }, [queryGroups, selectedGroup, setAlertsQuery]); + + const { deleteQuery, setQuery } = useGlobalTime(); + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []); + + useInspectButton({ + deleteQuery, + loading: isLoadingGroups, + refetch, + request, + response, + setQuery, + uniqueQueryId, + }); + + const inspect = useMemo( + () => ( + + ), + [uniqueQueryId] + ); + + const takeActionItems = useGroupTakeActionsItems({ + indexName: indexPattern.title, + currentStatus: currentAlertStatusFilterValue, + showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, + }); + + const getTakeActionItems = useCallback( + (groupFilters: Filter[], groupNumber: number) => + takeActionItems({ + groupNumber, + query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, + selectedGroup, + tableId, + }), + [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] + ); + + return useMemo( + () => + getGrouping({ + activePage: pageIndex, + data: alertsGroupsData?.aggregations, + groupingLevel, + inspectButton: inspect, + isLoading: loading || isLoadingGroups, + itemsPerPage: pageSize, + onChangeGroupsItemsPerPage: (size: number) => setPageSize(size), + onChangeGroupsPage: (index) => setPageIndex(index), + renderChildComponent, + onGroupClose, + selectedGroup, + takeActionItems: getTakeActionItems, + }), + [ + alertsGroupsData?.aggregations, + getGrouping, + getTakeActionItems, + groupingLevel, + inspect, + isLoadingGroups, + loading, + pageIndex, + pageSize, + renderChildComponent, + onGroupClose, + selectedGroup, + setPageIndex, + setPageSize, + ] + ); +}; + +export const GroupedSubLevel = React.memo(GroupedSubLevelComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx index f84305dcb3b37..d663b2abc2c61 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx @@ -30,7 +30,7 @@ describe('useGroupTakeActionsItems', () => { groupNumber: 0, selectedGroup: 'test', }; - it('returns array take actions items available for alerts table if showAlertStatusActions is true', async () => { + it('returns all take actions items if showAlertStatusActions is true and currentStatus is undefined', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -47,7 +47,106 @@ describe('useGroupTakeActionsItems', () => { }); }); - it('returns empty array of take actions items available for alerts table if showAlertStatusActions is false', async () => { + it('returns all take actions items if currentStatus is []', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: [], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); + + it('returns all take actions items if currentStatus.length > 1', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['open', 'closed'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); + + it('returns acknowledged & closed take actions items if currentStatus === ["open"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['open'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('acknowledge'); + expect(currentParams[1].key).toEqual('close'); + }); + }); + + it('returns open & acknowledged take actions items if currentStatus === ["closed"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['closed'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('open'); + expect(currentParams[1].key).toEqual('acknowledge'); + }); + }); + + it('returns open & closed take actions items if currentStatus === ["acknowledged"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['acknowledged'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('open'); + expect(currentParams[1].key).toEqual('close'); + }); + }); + + it('returns empty take actions items if showAlertStatusActions is false', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -63,4 +162,20 @@ describe('useGroupTakeActionsItems', () => { expect(result.current(getActionItemsParams).length).toEqual(0); }); }); + it('returns array take actions items if showAlertStatusActions is true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx index d2baadb99d124..5d151d2e4cc88 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { Status } from '../../../../../common/detection_engine/schemas/common'; @@ -30,8 +30,9 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import * as i18n from '../translations'; import { getTelemetryEvent, METRIC_TYPE, track } from '../../../../common/lib/telemetry'; import type { StartServices } from '../../../../types'; + export interface TakeActionsProps { - currentStatus?: Status; + currentStatus?: Status[]; indexName: string; showAlertStatusActions?: boolean; } @@ -182,7 +183,7 @@ export const useGroupTakeActionsItems = ({ ] ); - const items = useMemo(() => { + return useMemo(() => { const getActionItems = ({ query, tableId, @@ -196,61 +197,89 @@ export const useGroupTakeActionsItems = ({ }) => { const actionItems: JSX.Element[] = []; if (showAlertStatusActions) { - if (currentStatus !== FILTER_OPEN) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_OPEN as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_OPEN_SELECTED} - - ); - } - if (currentStatus !== FILTER_ACKNOWLEDGED) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_ACKNOWLEDGED_SELECTED} - - ); - } - if (currentStatus !== FILTER_CLOSED) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_CLOSED as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_CLOSE_SELECTED} - + if (currentStatus && currentStatus.length === 1) { + const singleStatus = currentStatus[0]; + if (singleStatus !== FILTER_OPEN) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_OPEN as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_OPEN_SELECTED} + + ); + } + if (singleStatus !== FILTER_ACKNOWLEDGED) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_ACKNOWLEDGED_SELECTED} + + ); + } + if (singleStatus !== FILTER_CLOSED) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_CLOSED as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_CLOSE_SELECTED} + + ); + } + } else { + const statusArr = { + [FILTER_OPEN]: BULK_ACTION_OPEN_SELECTED, + [FILTER_ACKNOWLEDGED]: BULK_ACTION_ACKNOWLEDGED_SELECTED, + [FILTER_CLOSED]: BULK_ACTION_CLOSE_SELECTED, + }; + Object.keys(statusArr).forEach((workflowStatus) => + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: workflowStatus as AlertWorkflowStatus, + tableId, + }) + } + > + {statusArr[workflowStatus]} + + ) ); } } @@ -259,6 +288,4 @@ export const useGroupTakeActionsItems = ({ return getActionItems; }, [currentStatus, onClickUpdate, showAlertStatusActions]); - - return items; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts new file mode 100644 index 0000000000000..9e0b4e63715aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts @@ -0,0 +1,1736 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockAlertSearchResponse } from '../../../../common/components/alerts_treemap/lib/mocks/mock_alert_search_response'; + +export const groupingSearchResponse = { + ruleName: { + ...mockAlertSearchResponse, + hits: { + total: { + value: 6048, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + groupsCount: { + value: 32, + }, + groupByFields: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: ['critical hosts [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical users [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['critical users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['high users [Duplicate]', 'f'], + key_as_string: 'high users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['high users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['low users [Duplicate]', 'f'], + key_as_string: 'low users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['low users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['medium users [Duplicate]', 'f'], + key_as_string: 'medium users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['medium users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['critical hosts', 'f'], + key_as_string: 'critical hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts', 'f'], + key_as_string: 'high hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts ', 'f'], + key_as_string: 'low hosts |f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts', 'f'], + key_as_string: 'medium hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical users [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 91, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 91, + }, + { + key: 'rule', + doc_count: 91, + }, + ], + }, + unitsCount: { + value: 91, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 91, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + ], + }, + unitsCount: { + value: 6048, + }, + }, + }, + hostName: { + ...mockAlertSearchResponse, + hits: { + total: { + value: 900, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + groupsCount: { + value: 40, + }, + groupByFields: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'Host-f0m6ngo8fo', + doc_count: 75, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 75, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 75, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 25, + }, + }, + { + key: 'Host-4aijlqggv8', + doc_count: 63, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 63, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 63, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 21, + }, + }, + { + key: 'Host-e50lhbdm91', + doc_count: 51, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 51, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 51, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 17, + }, + }, + { + key: 'sqp', + doc_count: 42, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 42, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 42, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'sUl', + doc_count: 33, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 33, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 33, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'vLJ', + doc_count: 30, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 30, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 30, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Host-n28uwmsqmd', + doc_count: 27, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 27, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 27, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 9, + }, + }, + { + key: 'JaE', + doc_count: 27, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 27, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 27, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'CUA', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'FWT', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'ZqT', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'mmn', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'xRS', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'HiC', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Host-d7zbfvl3zz', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 7, + }, + }, + { + key: 'Nnc', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'OqH', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Vaw', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'XPg', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'qBS', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'rwt', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'xVJ', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Bxg', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'efP', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'qcb', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + ], + }, + unitsCount: { + value: 900, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts index 624b343c14cf9..921a8d3e3d43f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts @@ -42,8 +42,8 @@ export const getAlertsGroupingQuery = ({ getGroupingQuery({ additionalFilters, from, - groupByFields: !isNoneGroup(selectedGroup) ? getGroupFields(selectedGroup) : [], - statsAggregations: !isNoneGroup(selectedGroup) + groupByFields: !isNoneGroup([selectedGroup]) ? getGroupFields(selectedGroup) : [], + statsAggregations: !isNoneGroup([selectedGroup]) ? getAggregationsByGroupField(selectedGroup) : [], pageNumber: pageIndex * pageSize, @@ -51,7 +51,7 @@ export const getAlertsGroupingQuery = ({ { unitsCount: { value_count: { field: selectedGroup } }, }, - ...(!isNoneGroup(selectedGroup) + ...(!isNoneGroup([selectedGroup]) ? [{ groupsCount: { cardinality: { field: selectedGroup } } }] : []), ], diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx deleted file mode 100644 index 346e4b51df72d..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import type { Filter } from '@kbn/es-query'; -import useResizeObserver from 'use-resize-observer/polyfilled'; - -import '../../../common/mock/match_media'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, - TestProviders, -} from '../../../common/mock'; -import type { AlertsTableComponentProps } from './alerts_grouping'; -import { GroupedAlertsTableComponent } from './alerts_grouping'; -import { TableId } from '@kbn/securitysolution-data-table'; -import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; -import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; -import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; -import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; -import type { State } from '../../../common/store'; -import { createStore } from '../../../common/store'; -import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; - -jest.mock('@kbn/securitysolution-grouping'); - -jest.mock('../../../common/containers/sourcerer'); -jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest.fn().mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: jest.fn(), - }), -})); - -jest.mock('./grouping_settings', () => ({ - getAlertsGroupingQuery: jest.fn(), - getDefaultGroupingOptions: () => [ - { label: 'ruleName', key: 'kibana.alert.rule.name' }, - { label: 'userName', key: 'user.name' }, - { label: 'hostName', key: 'host.name' }, - { label: 'sourceIP', key: 'source.ip' }, - ], - getSelectedGroupBadgeMetrics: jest.fn(), - getSelectedGroupButtonContent: jest.fn(), - getSelectedGroupCustomMetrics: jest.fn(), - useGroupTakeActionsItems: jest.fn(), -})); - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); -jest.mock('../../../common/utils/normalize_time_range'); - -const mockUseFieldBrowserOptions = jest.fn(); -jest.mock('../../../timelines/components/fields_browser', () => ({ - useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), -})); - -const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; -jest.mock('use-resize-observer/polyfilled'); -mockUseResizeObserver.mockImplementation(() => ({})); - -const mockFilterManager = createFilterManagerMock(); - -const mockKibanaServices = createStartServicesMock(); - -jest.mock('../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../common/lib/kibana'); - - return { - ...original, - useUiSetting$: jest.fn().mockReturnValue([]), - useKibana: () => ({ - services: { - ...mockKibanaServices, - application: { - navigateToUrl: jest.fn(), - capabilities: { - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - cases: { - ui: { getCasesContext: mockCasesContext }, - }, - uiSettings: { - get: jest.fn(), - }, - timelines: { ...mockTimelines }, - data: { - query: { - filterManager: mockFilterManager, - }, - }, - docLinks: { - links: { - siem: { - privileges: 'link', - }, - }, - }, - storage: { - get: jest.fn(), - set: jest.fn(), - }, - triggerActionsUi: { - getAlertsStateTable: jest.fn(() => <>), - alertsTableConfigurationRegistry: {}, - }, - }, - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - remove: jest.fn(), - }), - }; -}); -const state: State = { - ...mockGlobalState, -}; -const { storage } = createSecuritySolutionStorageMock(); -const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - -const groupingStore = createStore( - { - ...state, - groups: { - groupSelector: <>, - selectedGroup: 'host.name', - }, - }, - SUB_PLUGINS_REDUCER, - kibanaObservable, - storage -); - -jest.mock('./timeline_actions/use_add_bulk_to_timeline', () => ({ - useAddBulkToTimelineAction: jest.fn(() => {}), -})); - -const sourcererDataView = { - indicesExist: true, - loading: false, - indexPattern: { - fields: [], - }, - browserFields: {}, -}; -const renderChildComponent = (groupingFilters: Filter[]) =>

; - -const testProps: AlertsTableComponentProps = { - defaultFilters: [], - from: '2020-07-07T08:20:18.966Z', - globalFilters: [], - globalQuery: { - query: 'query', - language: 'language', - }, - hasIndexMaintenance: true, - hasIndexWrite: true, - loading: false, - renderChildComponent, - runtimeMappings: {}, - signalIndexName: 'test', - tableId: TableId.test, - to: '2020-07-08T08:20:18.966Z', -}; - -const resetPagination = jest.fn(); - -describe('GroupedAlertsTable', () => { - const getGrouping = jest.fn().mockReturnValue(); - beforeEach(() => { - jest.clearAllMocks(); - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - selectedPatterns: ['myFakebeat-*'], - }); - (isNoneGroup as jest.Mock).mockReturnValue(true); - (useGrouping as jest.Mock).mockReturnValue({ - groupSelector: <>, - getGrouping, - selectedGroup: 'host.name', - pagination: { pageSize: 1, pageIndex: 0, reset: resetPagination }, - }); - }); - - it('calls the proper initial dispatch actions for groups', () => { - render( - - - - ); - expect(mockDispatch).toHaveBeenCalledTimes(2); - expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR' - ); - expect(mockDispatch.mock.calls[1][0].type).toEqual( - 'x-pack/security_solution/groups/UPDATE_SELECTED_GROUP' - ); - }); - - it('renders grouping table', async () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - - const { getByTestId } = render( - - - - ); - expect(getByTestId('grouping-table')).toBeInTheDocument(); - expect(getGrouping.mock.calls[0][0].isLoading).toEqual(false); - }); - - it('renders loading when expected', () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - render( - - - - ); - expect(getGrouping.mock.calls[0][0].isLoading).toEqual(true); - }); - - it('resets grouping pagination when global query updates', () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - const { rerender } = render( - - - - ); - // called on initial query definition - expect(resetPagination).toHaveBeenCalledTimes(1); - rerender( - - - - ); - expect(resetPagination).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx index 78d736e99c93e..92f77c3e2df91 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { isNoneGroup } from '@kbn/securitysolution-grouping'; import { dataTableSelectors, tableDefaults, @@ -29,9 +28,6 @@ export const getPersistentControlsHook = (tableId: TableId) => { const getGroupSelector = groupSelectors.getGroupSelector(); const groupSelector = useSelector((state: State) => getGroupSelector(state)); - const getSelectedGroup = groupSelectors.getSelectedGroup(); - - const selectedGroup = useSelector((state: State) => getSelectedGroup(state)); const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); @@ -88,10 +84,10 @@ export const getPersistentControlsHook = (tableId: TableId) => { hasRightOffset={false} additionalFilters={additionalFiltersComponent} showInspect={false} - additionalMenuOptions={isNoneGroup(selectedGroup) ? [groupSelector] : []} + additionalMenuOptions={groupSelector != null ? [groupSelector] : []} /> ), - [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector, selectedGroup] + [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector] ); return { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 64e489599dd05..13836e658eee7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React from 'react'; -import { mount } from 'enzyme'; +import React, { useEffect } from 'react'; +import { render, waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; -import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { createSecuritySolutionStorageMock, @@ -29,6 +28,10 @@ import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_cont import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { useListsConfig } from '../../containers/detection_engine/lists/use_lists_config'; +import type { FilterGroupProps } from '../../../common/components/filter_group/types'; +import { FilterGroup } from '../../../common/components/filter_group'; +import type { AlertsTableComponentProps } from '../../components/alerts_table/alerts_grouping'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -38,6 +41,27 @@ jest.mock('../../../common/components/search_bar', () => ({ jest.mock('../../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../../common/hooks/use_space_id', () => ({ + useSpaceId: () => 'default', +})); +jest.mock('../../../common/components/filter_group'); + +const mockStatusCapture = jest.fn(); +const GroupedAlertsTable: React.FC = ({ + currentAlertStatusFilterValue, +}) => { + useEffect(() => { + if (currentAlertStatusFilterValue) { + mockStatusCapture(currentAlertStatusFilterValue); + } + }, [currentAlertStatusFilterValue]); + return ; +}; + +jest.mock('../../components/alerts_table/alerts_grouping', () => ({ + GroupedAlertsTable, +})); + jest.mock('../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../components/user_info'); jest.mock('../../../common/containers/sourcerer'); @@ -158,9 +182,11 @@ jest.mock('../../../common/components/page/use_refetch_by_session'); describe('DetectionEnginePageComponent', () => { beforeAll(() => { + (useListsConfig as jest.Mock).mockReturnValue({ loading: false, needsConfiguration: false }); (useParams as jest.Mock).mockReturnValue({}); (useUserData as jest.Mock).mockReturnValue([ { + loading: false, hasIndexRead: true, canUserREAD: true, }, @@ -170,10 +196,15 @@ describe('DetectionEnginePageComponent', () => { indexPattern: {}, browserFields: mockBrowserFields, }); + (FilterGroup as jest.Mock).mockImplementation(() => { + return ; + }); + }); + beforeEach(() => { + jest.clearAllMocks(); }); - it('renders correctly', async () => { - const wrapper = mount( + const { getByTestId } = render( @@ -181,12 +212,12 @@ describe('DetectionEnginePageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('FiltersGlobal').exists()).toBe(true); + expect(getByTestId('filter-group__loading')).toBeInTheDocument(); }); }); it('renders the chart panels', async () => { - const wrapper = mount( + const { getByTestId } = render( @@ -195,7 +226,119 @@ describe('DetectionEnginePageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('[data-test-subj="chartPanels"]').exists()).toBe(true); + expect(getByTestId('chartPanels')).toBeInTheDocument(); + }); + }); + + it('the pageFiltersUpdateHandler updates status when a multi status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.workflow_status', + params: ['open', 'acknowledged'], + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); + }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open', 'acknowledged']); + }); + + it('the pageFiltersUpdateHandler updates status when a single status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.workflow_status', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.severity', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.severity': 'low', + }, + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); + }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open']); + }); + + it('the pageFiltersUpdateHandler clears status when no status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.severity', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.severity': 'low', + }, + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, []); }); }); 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 0eacecd46e8fc..1ccfaea4584ee 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 @@ -29,7 +29,6 @@ import { dataTableActions, dataTableSelectors, tableDefaults, - FILTER_OPEN, TableId, } from '@kbn/securitysolution-data-table'; import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../common/constants'; @@ -139,7 +138,7 @@ const DetectionEnginePageComponent: React.FC = ({ const arePageFiltersEnabled = useIsExperimentalFeatureEnabled('alertsPageFiltersEnabled'); // when arePageFiltersEnabled === false - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const [statusFilter, setStatusFilter] = useState([]); const updatedAt = useShallowEqualSelector( (state) => (getTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults).updated @@ -177,8 +176,8 @@ const DetectionEnginePageComponent: React.FC = ({ if (arePageFiltersEnabled) { return detectionPageFilters; } - return buildAlertStatusFilter(filterGroup); - }, [filterGroup, detectionPageFilters, arePageFiltersEnabled]); + return buildAlertStatusFilter(statusFilter[0] ?? 'open'); + }, [statusFilter, detectionPageFilters, arePageFiltersEnabled]); useEffect(() => { if (!detectionPageFilterHandler) return; @@ -276,6 +275,19 @@ const DetectionEnginePageComponent: React.FC = ({ const pageFiltersUpdateHandler = useCallback((newFilters: Filter[]) => { setDetectionPageFilters(newFilters); + if (newFilters.length) { + const newStatusFilter = newFilters.find( + (filter) => filter.meta.key === 'kibana.alert.workflow_status' + ); + if (newStatusFilter) { + const status: Status[] = newStatusFilter.meta.params + ? (newStatusFilter.meta.params as Status[]) + : [newStatusFilter.query?.match_phrase['kibana.alert.workflow_status']]; + setStatusFilter(status); + } else { + setStatusFilter([]); + } + } }, []); // Callback for when open/closed filter changes @@ -284,9 +296,9 @@ const DetectionEnginePageComponent: React.FC = ({ const timelineId = TableId.alertsOnAlertsPage; clearEventsLoading({ id: timelineId }); clearEventsDeleted({ id: timelineId }); - setFilterGroup(newFilterGroup); + setStatusFilter([newFilterGroup]); }, - [clearEventsLoading, clearEventsDeleted, setFilterGroup] + [clearEventsLoading, clearEventsDeleted, setStatusFilter] ); const areDetectionPageFiltersLoading = useMemo(() => { @@ -317,7 +329,7 @@ const DetectionEnginePageComponent: React.FC = ({ @@ -352,7 +364,7 @@ const DetectionEnginePageComponent: React.FC = ({ [ arePageFiltersEnabled, dataViewId, - filterGroup, + statusFilter, filters, onFilterGroupChangedCallback, pageFiltersUpdateHandler, @@ -462,7 +474,7 @@ const DetectionEnginePageComponent: React.FC = ({ { const [updatedAt, setUpdatedAt] = useState(Date.now()); const { toggleStatus, setToggleStatus } = useQueryToggle(TABLE_QUERY_ID); - const { deleteQuery, setQuery, from, to } = useGlobalTime(false); + const { deleteQuery, setQuery, from, to } = useGlobalTime(); const { isLoading: isSearchLoading, data, diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx index 28e5c696d1f52..85cc8c0043897 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx @@ -41,7 +41,7 @@ const HOST_RISK_QUERY_ID = 'hostRiskScoreKpiQuery'; const USER_RISK_QUERY_ID = 'userRiskScoreKpiQuery'; export const EntityAnalyticsHeader = () => { - const { from, to } = useGlobalTime(false); + const { from, to } = useGlobalTime(); const timerange = useMemo( () => ({ from, From b5c88d90ceac26e990dcde8c5e8df524cad0a2a8 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 24 Apr 2023 09:07:15 -0400 Subject: [PATCH 04/13] [Security Solution] Move policy meta updates to policy update (#155462) ## Summary Moving the new meta fields in Policy to update when the Policy update callback is called. These fields are used in telemetry. These fields are being moved from the Policy watcher to avoid triggering a policy deploy on many Agents at once on upgrade. Instead, these fields will be updated whenever the next Endpoint policy update comes. New Policies will have the telemetry fields already populated. ### Checklist - [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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../endpoint/endpoint_app_context_services.ts | 3 +- .../endpoint/lib/policy/license_watch.test.ts | 26 +------ .../endpoint/lib/policy/license_watch.ts | 7 -- .../fleet_integration.test.ts | 69 ++++++++++++++++++- .../fleet_integration/fleet_integration.ts | 30 +++++++- .../security_solution/server/plugin.ts | 1 - 6 files changed, 100 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 489953a2d96d2..8e1855b2cd84c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -125,7 +125,8 @@ export class EndpointAppContextService { logger, licenseService, featureUsageService, - endpointMetadataService + endpointMetadataService, + cloud ) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts index 9c57b8156e3e3..9d962bc0e64ce 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts @@ -16,7 +16,6 @@ import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks'; import { PolicyWatcher } from './license_watch'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; -import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; @@ -36,7 +35,6 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa describe('Policy-Changing license watcher', () => { const logger = loggingSystemMock.create().get('license_watch.test'); - const cloudServiceMock = cloudMock.createSetup(); const soStartMock = savedObjectsServiceMock.createStartContract(); const esStartMock = elasticsearchServiceMock.createStart(); let packagePolicySvcMock: jest.Mocked; @@ -53,13 +51,7 @@ describe('Policy-Changing license watcher', () => { // mock a license-changing service to test reactivity const licenseEmitter: Subject = new Subject(); const licenseService = new LicenseService(); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // swap out watch function, just to ensure it gets called when a license change happens const mockWatch = jest.fn(); @@ -104,13 +96,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); await pw.watch(Gold); // just manually trigger with a given license expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts @@ -137,13 +123,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // emulate a license change below paid tier await pw.watch(Basic); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index dded85b559c6d..195c8509e60d5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -17,7 +17,6 @@ import type { } from '@kbn/core/server'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; -import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; @@ -35,19 +34,16 @@ export class PolicyWatcher { private policyService: PackagePolicyClient; private subscription: Subscription | undefined; private soStart: SavedObjectsServiceStart; - private cloud: CloudSetup; constructor( policyService: PackagePolicyClient, soStart: SavedObjectsServiceStart, esStart: ElasticsearchServiceStart, - cloud: CloudSetup, logger: Logger ) { this.policyService = policyService; this.esClient = esStart.client.asInternalUser; this.logger = logger; this.soStart = soStart; - this.cloud = cloud; } /** @@ -105,9 +101,6 @@ export class PolicyWatcher { for (const policy of response.items as PolicyData[]) { const updatePolicy = getPolicyDataForUpdate(policy); const policyConfig = updatePolicy.inputs[0].config.policy.value; - updatePolicy.inputs[0].config.policy.value.meta.license = license.type || ''; - // add cloud info to policy meta - updatePolicy.inputs[0].config.policy.value.meta.cloud = this.cloud?.isCloudEnabled; try { if (!isEndpointPolicyValidForLicense(policyConfig, license)) { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 30c7e776e718e..c415fc287fbec 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -366,7 +366,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -382,7 +383,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -412,7 +414,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -427,6 +430,66 @@ describe('ingest_integration tests ', () => { }); }); + describe('package policy update callback when meta fields should be updated', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + beforeEach(() => { + licenseEmitter.next(Platinum); // set license level to platinum + }); + it('updates successfully when meta fields differ from services', async () => { + const mockPolicy = policyFactory(); + mockPolicy.meta.cloud = true; // cloud mock will return true + mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyUpdateCallback( + logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService + ); + const policyConfig = generator.generatePolicyPackagePolicy(); + // values should be updated + policyConfig.inputs[0]!.config!.policy.value.meta.cloud = false; + policyConfig.inputs[0]!.config!.policy.value.meta.license = 'gold'; + const updatedPolicyConfig = await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); + }); + + it('meta fields stay the same where there is no difference', async () => { + const mockPolicy = policyFactory(); + mockPolicy.meta.cloud = true; // cloud mock will return true + mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyUpdateCallback( + logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService + ); + const policyConfig = generator.generatePolicyPackagePolicy(); + // values should be updated + policyConfig.inputs[0]!.config!.policy.value.meta.cloud = true; + policyConfig.inputs[0]!.config!.policy.value.meta.license = 'platinum'; + const updatedPolicyConfig = await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); + }); + }); + describe('package policy delete callback', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index fd6f85af1a045..7e68c63f07593 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -44,6 +44,17 @@ const isEndpointPackagePolicy = ( return packagePolicy.package?.name === 'endpoint'; }; +const shouldUpdateMetaValues = ( + endpointPackagePolicy: PolicyConfig, + currentLicenseType: string, + currentCloudInfo: boolean +) => { + return ( + endpointPackagePolicy.meta.license !== currentLicenseType || + endpointPackagePolicy.meta.cloud !== currentCloudInfo + ); +}; + /** * Callback to handle creation of PackagePolicies in Fleet */ @@ -152,7 +163,8 @@ export const getPackagePolicyUpdateCallback = ( logger: Logger, licenseService: LicenseService, featureUsageService: FeatureUsageService, - endpointMetadataService: EndpointMetadataService + endpointMetadataService: EndpointMetadataService, + cloud: CloudSetup ): PutPackagePolicyUpdateCallback => { return async (newPackagePolicy: NewPackagePolicy): Promise => { if (!isEndpointPackagePolicy(newPackagePolicy)) { @@ -170,6 +182,22 @@ export const getPackagePolicyUpdateCallback = ( notifyProtectionFeatureUsage(newPackagePolicy, featureUsageService, endpointMetadataService); + const newEndpointPackagePolicy = newPackagePolicy.inputs[0].config?.policy + ?.value as PolicyConfig; + + if ( + newPackagePolicy.inputs[0].config?.policy?.value && + shouldUpdateMetaValues( + newEndpointPackagePolicy, + licenseService.getLicenseType(), + cloud?.isCloudEnabled + ) + ) { + newEndpointPackagePolicy.meta.license = licenseService.getLicenseType(); + newEndpointPackagePolicy.meta.cloud = cloud?.isCloudEnabled; + newPackagePolicy.inputs[0].config.policy.value = newEndpointPackagePolicy; + } + return newPackagePolicy; }; }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ab5ac00454a5e..e6ad26f1f405f 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -472,7 +472,6 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.fleet.packagePolicyService, core.savedObjects, core.elasticsearch, - plugins.cloud, logger ); this.policyWatcher.start(licenseService); From b71f7831d80785df4d07831388d5d082d31ec8d6 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Mon, 24 Apr 2023 15:22:56 +0200 Subject: [PATCH 05/13] [AO] Sync chart pointers on the metric threshold alert details page (#155402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #155354 ## Summary This PR syncs chart pointers on the metric threshold alert details page. ![image](https://user-images.githubusercontent.com/12370520/233380782-bf97eab8-167d-4f97-a10d-d2bfec1936e7.png) ## 🧪 How to test - Add `xpack.observability.unsafe.alertDetails.metrics.enabled: true` to the Kibana config - Create a metric threshold rule with multiple conditions that generates an alert - Go to the alert details page and check the chart pointers --- .../components/expression_chart.test.tsx | 3 +++ .../components/expression_chart.tsx | 21 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index f2bb22485fe9c..745a1f0169788 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -22,6 +22,9 @@ jest.mock('../../../hooks/use_kibana', () => ({ useKibanaContextForPlugin: () => ({ services: { ...mockStartServices, + charts: { + activeCursor: jest.fn(), + }, }, }), })); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 8dd7762feb6b9..8b453579b5e67 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useRef } from 'react'; import { Axis, Chart, @@ -17,6 +17,7 @@ import { } from '@elastic/charts'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useActiveCursor } from '@kbn/charts-plugin/public'; import { DataViewBase } from '@kbn/es-query'; import { first, last } from 'lodash'; @@ -66,7 +67,7 @@ export const ExpressionChart: React.FC = ({ timeRange, annotations, }) => { - const { uiSettings } = useKibanaContextForPlugin().services; + const { uiSettings, charts } = useKibanaContextForPlugin().services; const { isLoading, data } = useMetricsExplorerChartData( expression, @@ -77,6 +78,11 @@ export const ExpressionChart: React.FC = ({ timeRange ); + const chartRef = useRef(null); + const handleCursorUpdate = useActiveCursor(charts.activeCursor, chartRef, { + isDateHistogram: true, + }); + if (isLoading) { return ; } @@ -141,7 +147,7 @@ export const ExpressionChart: React.FC = ({ return ( <> - + = ({ tickFormat={createFormatterForMetric(metric)} domain={domain} /> - +

From 4dc21e5589067fe68ca2767204759e8579914c16 Mon Sep 17 00:00:00 2001 From: Apoorva Joshi <30438249+ajosh0504@users.noreply.github.com> Date: Mon, 24 Apr 2023 06:51:06 -0700 Subject: [PATCH 06/13] Updates to pre-built Security ML jobs (#154596) ## Summary This PR makes the following updates to the pre-built Security ML jobs: - Making the `security-packetbeat` compatible with Agent - Removing superfluous fields from the job configurations to make them consistent - Updating the `detector_description` field for almost all jobs - Adding influencers where missing and/or relevant - Adding a `job_revision` custom setting similar to the Logs [jobs](https://github.com/elastic/kibana/blob/main/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json#L29). Moving forward, this number will be updated each time a job is updated. We are starting with 4 since the `linux` and `windows` jobs are at v3 right now - Adding a `managed`: `true` tag to indicate that these jobs are pre-configured by Elastic and so users will see the warnings added in [this](https://github.com/elastic/kibana/pull/122305) PR if users choose to delete, or modify these jobs --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../modules/security_auth/manifest.json | 4 +- .../ml/auth_high_count_logon_events.json | 14 ++--- ...gh_count_logon_events_for_a_source_ip.json | 18 ++---- .../ml/auth_high_count_logon_fails.json | 14 ++--- .../ml/auth_rare_hour_for_a_user.json | 18 +++--- .../ml/auth_rare_source_ip_for_a_user.json | 18 +++--- .../security_auth/ml/auth_rare_user.json | 18 +++--- .../datafeed_suspicious_login_activity.json | 9 +-- .../ml/suspicious_login_activity.json | 27 +++------ .../modules/security_cloudtrail/manifest.json | 12 ++-- .../ml/high_distinct_count_error_message.json | 21 +++---- .../ml/rare_error_code.json | 20 +++---- .../ml/rare_method_for_a_city.json | 20 +++---- .../ml/rare_method_for_a_country.json | 20 +++---- .../ml/rare_method_for_a_username.json | 17 +++--- .../modules/security_linux/manifest.json | 7 +-- .../v3_linux_anomalous_network_activity.json | 49 +++-------------- ...linux_anomalous_network_port_activity.json | 49 +++-------------- .../v3_linux_anomalous_process_all_hosts.json | 49 +++-------------- .../ml/v3_linux_anomalous_user_name.json | 48 +++------------- ...linux_network_configuration_discovery.json | 51 +++-------------- ...v3_linux_network_connection_discovery.json | 51 +++-------------- .../ml/v3_linux_rare_metadata_process.json | 30 +++------- .../ml/v3_linux_rare_metadata_user.json | 29 +++------- .../ml/v3_linux_rare_sudo_user.json | 51 +++-------------- .../ml/v3_linux_rare_user_compiler.json | 43 +++------------ ...v3_linux_system_information_discovery.json | 51 +++-------------- .../ml/v3_linux_system_process_discovery.json | 51 +++-------------- .../ml/v3_linux_system_user_discovery.json | 49 +++-------------- .../ml/v3_rare_process_by_host_linux.json | 48 +++------------- .../modules/security_network/manifest.json | 4 +- .../ml/high_count_by_destination_country.json | 14 ++--- .../ml/high_count_network_denies.json | 14 ++--- .../ml/high_count_network_events.json | 14 ++--- .../ml/rare_destination_country.json | 11 ++-- .../modules/security_packetbeat/manifest.json | 10 ++-- .../ml/datafeed_packetbeat_dns_tunneling.json | 16 +++--- ...datafeed_packetbeat_rare_dns_question.json | 16 +++--- .../datafeed_packetbeat_rare_user_agent.json | 16 +++--- .../ml/packetbeat_dns_tunneling.json | 29 +++------- .../ml/packetbeat_rare_dns_question.json | 22 ++------ .../ml/packetbeat_rare_server_domain.json | 24 ++------ .../ml/packetbeat_rare_urls.json | 23 ++------ .../ml/packetbeat_rare_user_agent.json | 23 ++------ .../ml/v3_rare_process_by_host_windows.json | 53 +++--------------- ...v3_windows_anomalous_network_activity.json | 53 +++--------------- .../v3_windows_anomalous_path_activity.json | 52 +++--------------- ...3_windows_anomalous_process_all_hosts.json | 55 +++---------------- ...v3_windows_anomalous_process_creation.json | 53 +++--------------- .../ml/v3_windows_anomalous_script.json | 42 +++----------- .../ml/v3_windows_anomalous_service.json | 37 +++---------- .../ml/v3_windows_anomalous_user_name.json | 53 +++--------------- .../ml/v3_windows_rare_metadata_process.json | 34 +++--------- .../ml/v3_windows_rare_metadata_user.json | 33 +++-------- .../ml/v3_windows_rare_user_runas_event.json | 46 ++-------------- ...windows_rare_user_type10_remote_login.json | 46 ++-------------- 56 files changed, 386 insertions(+), 1313 deletions(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json index b3395d82a9c29..d600e4a637acf 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json @@ -2,7 +2,7 @@ "id": "security_auth", "title": "Security: Authentication", "description": "Detect anomalous activity in your ECS-compatible authentication logs.", - "type": "auth data", + "type": "Auth data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*,filebeat-*,winlogbeat-*", "query": { @@ -14,7 +14,7 @@ } } ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json index 7ca7a5ebd71e4..ac50e2f53535c 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json @@ -1,20 +1,16 @@ { "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration, or brute force activity.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of logon events", + "detector_description": "Detects high count of logon events.", "function": "high_non_zero_count", "detector_index": 0 } ], - "influencers": [], - "model_prune_window": "30d" + "influencers": ["source.ip", "winlog.event_data.LogonType", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -25,6 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Logon Events" + "security_app_display_name": "Spike in Logon Events", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json index 47096f4c6413f..d23f8df88ef6a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json @@ -1,25 +1,17 @@ { "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events from a particular source IP address. This can be due to password spraying, user enumeration, or brute force activity.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of auth events for a source IP", + "detector_description": "Detects high count of auth events for a source IP.", "function": "high_non_zero_count", "by_field_name": "source.ip", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "winlog.event_data.LogonType", - "user.name" - ], - "model_prune_window": "30d" + "influencers": ["source.ip", "winlog.event_data.LogonType", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Logon Events from a Source IP" + "security_app_display_name": "Spike in Logon Events from a Source IP", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json index 48586ef642ca6..db2db5ea00832 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json @@ -1,20 +1,16 @@ { "description": "Security: Authentication - Looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration, or brute force activity and may be a precursor to account takeover or credentialed access.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of logon fails", + "detector_description": "Detects high count of logon fails.", "function": "high_non_zero_count", "detector_index": 0 } ], - "influencers": [], - "model_prune_window": "30d" + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -25,6 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Failed Logon Events" + "security_app_display_name": "Spike in Failed Logon Events", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json index 1f421ed298b9f..57477497aeb62 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json @@ -1,23 +1,17 @@ { - "description": "Security: Authentication - looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare hour for a user", + "detector_description": "Detects rare hour for a user.", "function": "time_of_day", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Unusual Hour for a User to Logon" + "security_app_display_name": "Unusual Hour for a User to Logon", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json index 98a249074a67a..81185ef5039c7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json @@ -1,24 +1,18 @@ { - "description": "Security: Authentication - looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare source IP for a user", + "detector_description": "Detects rare source IP for a user.", "function": "rare", "by_field_name": "source.ip", "partition_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Unusual Source IP for a User to Logon from" + "security_app_display_name": "Unusual Source IP for a User to Logon from", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json index e2488480e61d1..58530fe085014 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json @@ -1,23 +1,17 @@ { - "description": "Security: Authentication - looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare user", + "detector_description": "Detects rare user authentication.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Rare User Logon" + "security_app_display_name": "Rare User Logon", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json index 386b9fab25667..59a9129e7b7bf 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json @@ -1,15 +1,10 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": { "event.category": "authentication" }}, - {"term": { "agent.type": "auditbeat" }} - ] + "filter": [{ "term": { "event.category": "authentication" } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json index 00e810b5348e7..bbe420b3ec0eb 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json @@ -1,24 +1,17 @@ { - "description": "Security: Auditbeat - Detect unusually high number of authentication attempts.", - "groups": [ - "security", - "auditbeat", - "authentication" - ], + "description": "Security: Authentication - Detects unusually high number of authentication attempts.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high number of authentication attempts", + "detector_description": "Detects high number of authentication attempts for a host.", "function": "high_non_zero_count", - "partition_field_name": "host.name" + "partition_field_name": "host.name", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "source.ip" - ], + "influencers": ["host.name", "user.name", "source.ip"], "model_prune_window": "30d" }, "allow_lazy_open": true, @@ -31,11 +24,7 @@ "custom_settings": { "created_by": "ml-module-security-auth", "security_app_display_name": "Unusual Login Activity", - "custom_urls": [ - { - "url_name": "IP Address Details", - "url_value": "security/network/ml-network/ip/$source.ip$?_g=()&query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ] + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json index 93797b9e3e758..52b406a0da7cb 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json @@ -1,16 +1,14 @@ { "id": "security_cloudtrail", "title": "Security: Cloudtrail", - "description": "Detect suspicious activity recorded in your cloudtrail logs.", - "type": "Filebeat data", + "description": "Detect suspicious activity recorded in Cloudtrail logs.", + "type": "Cloudtrail data", "logoFile": "logo.json", - "defaultIndexPattern": "filebeat-*", + "defaultIndexPattern": "logs-*,filebeat-*", "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "aws.cloudtrail"}} - ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "filter": [{ "term": { "event.dataset": "aws.cloudtrail" } }], + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json index 11b5f4625a484..2ba7c4fdf4085 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json @@ -1,24 +1,17 @@ { "description": "Security: Cloudtrail - Looks for a spike in the rate of an error message which may simply indicate an impending service failure but these can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_distinct_count(\"aws.cloudtrail.error_message\")", + "detector_description": "Detects high distinct count of Cloudtrail error messages.", "function": "high_distinct_count", - "field_name": "aws.cloudtrail.error_message" + "field_name": "aws.cloudtrail.error_message", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ], - "model_prune_window": "30d" + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Spike in AWS Error Messages" + "security_app_display_name": "Spike in AWS Error Messages", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json index c54c8e8378f2c..7752430876e3f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json @@ -1,23 +1,17 @@ { "description": "Security: Cloudtrail - Looks for unusual errors. Rare and unusual errors may simply indicate an impending service failure but they can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"aws.cloudtrail.error_code\"", + "detector_description": "Detects rare Cloudtrail error codes.", "function": "rare", - "by_field_name": "aws.cloudtrail.error_code" + "by_field_name": "aws.cloudtrail.error_code", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Rare AWS Error Code" + "security_app_display_name": "Rare AWS Error Code", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json index 2ed28884be94f..f7be6fe8cc8d7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json @@ -1,24 +1,18 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (city) that is unusual. This can be the result of compromised credentials or keys.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"source.geo.city_name\"", + "detector_description": "Detects rare event actions for a city.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "source.geo.city_name" + "partition_field_name": "source.geo.city_name", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual City for an AWS Command" + "security_app_display_name": "Unusual City for an AWS Command", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json index 1f14357e73444..d73f51f34de3a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json @@ -1,24 +1,18 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (country) that is unusual. This can be the result of compromised credentials or keys.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"source.geo.country_iso_code\"", + "detector_description": "Detects rare event actions for an ISO code.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "source.geo.country_iso_code" + "partition_field_name": "source.geo.country_iso_code", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.country_iso_code" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.country_iso_code"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual Country for an AWS Command" + "security_app_display_name": "Unusual Country for an AWS Command", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json index 76cce7fb829ca..a508028619833 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json @@ -1,23 +1,22 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a user context that does not normally call the method. This can be the result of compromised credentials or keys as someone uses a valid account to persist, move laterally, or exfil data.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"user.name\"", + "detector_description": "Detects rare event actions for a user.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "user.name" + "partition_field_name": "user.name", + "detector_index": 0 } ], "influencers": [ "user.name", "source.ip", - "source.geo.city_name" + "source.geo.city_name", + "aws.cloudtrail.user_identity.arn" ] }, "allow_lazy_open": true, @@ -29,6 +28,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual AWS Command for a User" + "security_app_display_name": "Unusual AWS Command for a User", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json index 269f90dea4471..cfff61e304c0e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json @@ -2,7 +2,7 @@ "id": "security_linux_v3", "title": "Security: Linux", "description": "Anomaly detection jobs for Linux host-based threat hunting and detection.", - "type": "linux data", + "type": "Linux data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*", "query": { @@ -43,10 +43,7 @@ ], "must_not": { "terms": { - "_tier": [ - "data_frozen", - "data_cold" - ] + "_tier": ["data_frozen", "data_cold"] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json index 29f6bf1d98412..b276bcc7856ba 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "network", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", - "by_field_name": "process.name" + "by_field_name": "process.name", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4004", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Activity" + "security_app_display_name": "Unusual Linux Network Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json index 34b97358260ac..a551d6c2c204f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "network" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare destination.port values.", + "detector_description": "Detects rare destination ports.", "function": "rare", - "by_field_name": "destination.port" + "by_field_name": "destination.port", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4005", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Port Activity" + "security_app_display_name": "Unusual Linux Network Port Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json index a20a508391fb9..dea5fa3a5db31 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json @@ -1,65 +1,30 @@ { "description": "Security: Linux - Looks for processes that are unusual to all Linux hosts. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "512mb", - "categorization_examples_limit": 4 - + "model_memory_limit": "512mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4003", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Process for a Linux Population" + "security_app_display_name": "Anomalous Process for a Linux Population", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json index 72be89bd79aad..05d46860b145f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json @@ -1,64 +1,30 @@ { "description": "Security: Linux - Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4008", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Username" + "security_app_display_name": "Unusual Linux Username", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json index 1481b7a03a559..fccfa9493e8c2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system network configuration discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network configuration discovery to increase their understanding of connected networks and hosts. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "40012", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Network Configuration Discovery" + "security_app_display_name": "Unusual Linux Network Configuration Discovery", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json index 2b1cf43ac94d3..32dc04c079db1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system network connection discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network connection discovery to increase their understanding of connected services and systems. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4013", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Connection Discovery" + "security_app_display_name": "Unusual Linux Network Connection Discovery", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json index fcec32acd69b5..6897876ad6ba3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json @@ -1,46 +1,30 @@ { "description": "Security: Linux - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "process.name" - ] + "influencers": ["host.name", "user.name", "process.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4009", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "security_app_display_name": "Unusual Linux Process Calling the Metadata Service" + "security_app_display_name": "Unusual Linux Process Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json index d8414c8bf22bd..ad81023d69383 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json @@ -1,45 +1,30 @@ { "description": "Security: Linux - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name" - ] + "influencers": ["host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4010", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "security_app_display_name": "Unusual Linux User Calling the Metadata Service" + "security_app_display_name": "Unusual Linux User Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json index a99e5f95572f7..11be6277c4220 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for sudo activity from an unusual user context. Unusual user context changes can be due to privilege escalation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4017", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Sudo Activity" + "security_app_display_name": "Unusual Sudo Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json index 9c8ca5316ace3..08dbbc60d02f7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privilege elevation via locally run exploits or malware activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.title", - "host.name", - "process.working_directory", - "user.name" - ] + "influencers": ["process.title", "host.name", "process.working_directory", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,24 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4018", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Linux Compiler Activity" + "security_app_display_name": "Anomalous Linux Compiler Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json index 0202854934285..255d0347654b0 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system information discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system information discovery to gather detailed information about system configuration and software versions. This may be a precursor to the selection of a persistence mechanism or a method of privilege elevation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4014", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Information Discovery Activity" + "security_app_display_name": "Unusual Linux System Information Discovery Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json index 23e6e607ccf08..03e57ce2237af 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system process discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system process discovery to increase their understanding of software applications running on a target host or network. This may be a precursor to the selection of a persistence mechanism or a method of privilege elevation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4015", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Process Discovery Activity" + "security_app_display_name": "Unusual Linux Process Discovery Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json index 8659e7a8f1f91..2b1c4dc595777 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system user or owner discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system owner or user discovery to identify currently active or primary users of a system. This may be a precursor to additional discovery, credential dumping, or privilege elevation activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4016", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Owner or User Discovery Activity" + "security_app_display_name": "Unusual Linux User Discovery Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json index a072007a0f13c..ce0e7f413f676 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json @@ -1,65 +1,31 @@ { "description": "Security: Linux - Looks for processes that are unusual to a particular Linux host. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each host.name, detects rare process.name values.", + "detector_description": "Detects rare processes for a host.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "host.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4002", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Process for a Linux Host" + "security_app_display_name": "Unusual Process for a Linux Host", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json index bed522d4e954a..edf6c66a213bd 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json @@ -2,7 +2,7 @@ "id": "security_network", "title": "Security: Network", "description": "Detect anomalous network activity in your ECS-compatible network logs.", - "type": "network data", + "type": "Network data", "logoFile": "logo.json", "defaultIndexPattern": "logs-*,filebeat-*,packetbeat-*", "query": { @@ -14,7 +14,7 @@ } } ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json index 4479fe8f8c662..b19a3f0e27812 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_non_zero_count by \"destination.geo.country_name\"", + "detector_description": "Detects high count by country.", "function": "high_non_zero_count", "by_field_name": "destination.geo.country_name", "detector_index": 0 @@ -19,8 +16,7 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,6 +27,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Network Traffic to a Country" + "security_app_display_name": "Spike in Network Traffic to a Country", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json index 984bfea22fa2d..1477e951d3ce9 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_count", + "detector_description": "Detects high count of network denies.", "function": "high_count", "detector_index": 0 } @@ -18,8 +15,7 @@ "destination.as.organization.name", "source.ip", "destination.port" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +26,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Firewall Denies" + "security_app_display_name": "Spike in Firewall Denies", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json index ba740d581a27e..81b516204fbc1 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_count", + "detector_description": "Detects high count of network events.", "function": "high_count", "detector_index": 0 } @@ -18,8 +15,7 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +26,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Network Traffic" + "security_app_display_name": "Spike in Network Traffic", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json index 123b802c475fb..4b8799d65b746 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json @@ -1,14 +1,11 @@ { "description": "Security: Network - looks for an unusual destination country name in the network logs. This can be due to initial access, persistence, command-and-control, or exfiltration activity. For example, when a user clicks on a link in a phishing email or opens a malicious document, a request may be sent to download and run a payload from a server in a country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"destination.geo.country_name\"", + "detector_description": "Detects rare country names.", "function": "rare", "by_field_name": "destination.geo.country_name", "detector_index": 0 @@ -30,6 +27,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Network Traffic to Rare Destination Country" + "security_app_display_name": "Network Traffic to Rare Destination Country", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json index f7a65d0137f26..799363b8fbac1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json @@ -1,16 +1,14 @@ { "id": "security_packetbeat", "title": "Security: Packetbeat", - "description": "Detect suspicious network activity in Packetbeat data.", + "description": "Detect suspicious activity in Packetbeat data.", "type": "Packetbeat data", "logoFile": "logo.json", - "defaultIndexPattern": "packetbeat-*", + "defaultIndexPattern": "packetbeat-*,logs-*", "query": { "bool": { - "filter": [ - {"term": {"agent.type": "packetbeat"}} - ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json index 449c8af238b56..334435732a07e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "dns"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "dns" } }, + { "term": { "event.dataset": "network_traffic.dns" } } ], - "must_not": [ - {"bool": {"filter": {"term": {"destination.ip": "169.254.169.254"}}}} - ] + "minimum_should_match": 1, + "must_not": [{ "bool": { "filter": { "term": { "destination.ip": "169.254.169.254" } } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json index 3a4055eb55ba0..fe87d86ee352f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "dns"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "dns" } }, + { "term": { "event.dataset": "network_traffic.dns" } } ], - "must_not": [ - {"bool": {"filter": {"term": {"dns.question.type": "PTR"}}}} - ] + "minimum_should_match": 1, + "must_not": [{ "bool": { "filter": { "term": { "dns.question.type": "PTR" } } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json index 5986c326ea80f..79a297595d8d7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "http"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "http" } }, + { "term": { "event.dataset": "network_traffic.http" } } ], - "must_not": [ - {"wildcard": {"user_agent.original": {"value": "Mozilla*"}}} - ] + "minimum_should_match": 1, + "must_not": [{ "wildcard": { "user_agent.original": { "value": "Mozilla*" } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json index 313bd8e1bea39..54b8ddf2e7a14 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json @@ -1,23 +1,17 @@ { "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "dns" - ], + "groups": ["security", "packetbeat", "dns"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_info_content(\"dns.question.name\") over tld", + "detector_description": "Detects high info content of DNS questions over a population of TLDs.", "function": "high_info_content", "field_name": "dns.question.name", "over_field_name": "dns.question.etld_plus_one", "custom_rules": [ { - "actions": [ - "skip_result" - ], + "actions": ["skip_result"], "conditions": [ { "applies_to": "actual", @@ -29,12 +23,7 @@ ] } ], - "influencers": [ - "destination.ip", - "host.name", - "dns.question.etld_plus_one" - ], - "model_prune_window": "30d" + "influencers": ["destination.ip", "host.name", "dns.question.etld_plus_one"] }, "allow_lazy_open": true, "analysis_limits": { @@ -45,12 +34,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "DNS Tunneling" + "security_app_display_name": "DNS Tunneling", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json index 36c8b3acd722e..049d4e3babd23 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json @@ -1,22 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control activity.", - "groups": [ - "security", - "packetbeat", - "dns" - ], + "groups": ["security", "packetbeat", "dns"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"dns.question.name\"", + "detector_description": "Detects rare DNS question names.", "function": "rare", "by_field_name": "dns.question.name" } ], - "influencers": [ - "host.name" - ] + "influencers": ["host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -27,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual DNS Activity" + "security_app_display_name": "Unusual DNS Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json index 3f3c137e8fd34..d8df5c4986b99 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json @@ -1,24 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual HTTP or TLS destination domain activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"server.domain\"", + "detector_description": "Detects rare server domains.", "function": "rare", "by_field_name": "server.domain" } ], - "influencers": [ - "host.name", - "destination.ip", - "source.ip" - ] + "influencers": ["host.name", "destination.ip", "source.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Network Destination Domain Name" + "security_app_display_name": "Unusual Network Destination Domain Name", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json index afa430bd835f2..055204dd1c376 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json @@ -1,23 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual web browsing URL activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"url.full\"", + "detector_description": "Detects rare URLs.", "function": "rare", "by_field_name": "url.full" } ], - "influencers": [ - "host.name", - "destination.ip" - ] + "influencers": ["host.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Web Request" + "security_app_display_name": "Unusual Web Request", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json index bb2d524b41c1f..c947e4f1d509b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json @@ -1,23 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual HTTP user agent activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"user_agent.original\"", + "detector_description": "Detects rare web user agents.", "function": "rare", "by_field_name": "user_agent.original" } ], - "influencers": [ - "host.name", - "destination.ip" - ] + "influencers": ["host.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Web User Agent" + "security_app_display_name": "Unusual Web User Agent", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json index 6b7e5dcf56f1f..38fa9e2e4e904 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json @@ -1,67 +1,30 @@ { "description": "Security: Windows - Looks for processes that are unusual to a particular Windows host. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each host.name, detects rare process.name values.", + "detector_description": "Detects rare processes per host.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "host.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8001", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Process for a Windows Host" + "security_app_display_name": "Unusual Process for a Windows Host", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json index 04ee9912c15e3..2e04fa91be336 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", - "groups": [ - "endpoint", - "network", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "64mb", - "categorization_examples_limit": 4 + "model_memory_limit": "64mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8003", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Network Activity" + "security_app_display_name": "Unusual Windows Network Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json index d5c931b3c46e8..c9f0579309c6b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json @@ -1,65 +1,29 @@ { "description": "Security: Windows - Looks for activity in unusual paths that may indicate execution of malware or persistence mechanisms. Windows payloads often execute from user profile paths.", - "groups": [ - "endpoint", - "network", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.working_directory values.", + "detector_description": "Detects rare working directories.", "function": "rare", "by_field_name": "process.working_directory", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8004", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Path Activity" + "security_app_display_name": "Unusual Windows Path Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json index 1474763cec7b9..08baa6587f9ff 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Looks for processes that are unusual to all Windows hosts. Such unusual processes may indicate execution of unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.executable values.", + "detector_description": "Detects rare process executable values.", "function": "rare", - "by_field_name": "process.executable", + "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8002", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Process for a Windows Population" + "security_app_display_name": "Anomalous Process for a Windows Population", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json index 2966630fad878..1bf46c2d416a9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json @@ -1,67 +1,30 @@ { "description": "Security: Windows - Looks for unusual process relationships which may indicate execution of malware or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each process.parent.name, detects rare process.name values.", + "detector_description": "Detects rare processes per parent process.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "process.parent.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8005", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Windows Process Creation" + "security_app_display_name": "Anomalous Windows Process Creation", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json index b01641b2ef3ad..5472ad77e1b70 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json @@ -1,28 +1,17 @@ { "description": "Security: Windows - Looks for unusual powershell scripts that may indicate execution of malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "windows", - "winlogbeat", - "powershell", - "security" - ], + "groups": ["windows", "powershell", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects high information content in powershell.file.script_block_text values.", + "detector_description": "Detects high information content in powershell scripts.", "function": "high_info_content", - "field_name": "powershell.file.script_block_text" + "field_name": "powershell.file.script_block_text", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "file.path" - ] + "influencers": ["host.name", "user.name", "file.path"] }, "allow_lazy_open": true, "analysis_limits": { @@ -32,24 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8006", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Suspicious Powershell Script" + "security_app_display_name": "Suspicious Powershell Script", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json index 9716c8365e317..b2530538a9263 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json @@ -1,27 +1,17 @@ { - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "description": "Security: Windows - Looks for rare and unusual Windows service names which may indicate execution of unauthorized services, malware, or persistence mechanisms.", "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare winlog.event_data.ServiceName values.", + "detector_description": "Detects rare service names.", "function": "rare", - "by_field_name": "winlog.event_data.ServiceName" + "by_field_name": "winlog.event_data.ServiceName", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "winlog.event_data.ServiceName" - ] + "influencers": ["host.name", "winlog.event_data.ServiceName"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,20 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8007", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Service" + "security_app_display_name": "Unusual Windows Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json index eda4b768b5308..659e58cfdba32 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8008", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Username" + "security_app_display_name": "Unusual Windows Username", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json index ab4fd311d6646..953a00a8fff52 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json @@ -1,47 +1,29 @@ { "description": "Security: Windows - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "security", - "endpoint", - "process", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare process names.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "user.name" - ] + "influencers": ["process.name", "host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8011", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "security_app_display_name": "Unusual Windows Process Calling the Metadata Service" + "security_app_display_name": "Unusual Windows Process Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json index fe8a634d49921..df55cb3d67709 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json @@ -1,46 +1,29 @@ { "description": "Security: Windows - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "endpoint", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name" - ] + "influencers": ["host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8012", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "security_app_display_name": "Unusual Windows User Calling the Metadata Service" + "security_app_display_name": "Unusual Windows User Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json index b95aa1144f440..87d9d4b172f63 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json @@ -1,27 +1,16 @@ { "description": "Security: Windows - Unusual user context switches can be due to privilege escalation.", - "groups": [ - "endpoint", - "event-log", - "security", - "windows", - "winlogbeat", - "authentication" - ], + "groups": ["security", "windows", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name" } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +20,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8009", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows User Privilege Elevation Activity" + "security_app_display_name": "Unusual Windows User Privilege Elevation Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json index a6ec19401190f..e118f761453be 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json @@ -1,27 +1,16 @@ { "description": "Security: Windows - Unusual RDP (remote desktop protocol) user logins can indicate account takeover or credentialed access.", - "groups": [ - "endpoint", - "event-log", - "security", - "windows", - "winlogbeat", - "authentication" - ], + "groups": ["security", "windows", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name" } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +20,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8013", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Remote User" + "security_app_display_name": "Unusual Windows Remote User", + "managed": true, + "job_revision": 4 } } From 069324a823b2f7e2306c68ab81f913e9fd80472b Mon Sep 17 00:00:00 2001 From: Navarone Feekery <13634519+navarone-feekery@users.noreply.github.com> Date: Mon, 24 Apr 2023 15:59:36 +0200 Subject: [PATCH 07/13] [Enterprise Search] Use caching for filtered config fields (#155608) Moves the configurable fields filtering to the logic file so it can make use of caching. --- .../connector_configuration_form.tsx | 28 +- .../connector_configuration_logic.test.ts | 348 +++++++++++++++++- .../connector_configuration_logic.ts | 29 +- 3 files changed, 376 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx index 7627c5c869469..2c40eb0beafa4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx @@ -24,16 +24,12 @@ import { import { i18n } from '@kbn/i18n'; import { Status } from '../../../../../../common/types/api'; -import { DependencyLookup, DisplayType } from '../../../../../../common/types/connectors'; +import { DisplayType } from '../../../../../../common/types/connectors'; import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic'; import { ConnectorConfigurationField } from './connector_configuration_field'; -import { - ConfigEntry, - ConnectorConfigurationLogic, - dependenciesSatisfied, -} from './connector_configuration_logic'; +import { ConnectorConfigurationLogic } from './connector_configuration_logic'; export const ConnectorConfigurationForm = () => { const { status } = useValues(ConnectorConfigurationApiLogic); @@ -41,20 +37,6 @@ export const ConnectorConfigurationForm = () => { const { localConfigView } = useValues(ConnectorConfigurationLogic); const { saveConfig, setIsEditing } = useActions(ConnectorConfigurationLogic); - const dependencyLookup: DependencyLookup = localConfigView.reduce( - (prev: Record, configEntry: ConfigEntry) => ({ - ...prev, - [configEntry.key]: configEntry.value, - }), - {} - ); - - const filteredConfigView = localConfigView.filter( - (configEntry) => - configEntry.ui_restrictions.length <= 0 && - dependenciesSatisfied(configEntry.depends_on, dependencyLookup) - ); - return ( { @@ -63,7 +45,7 @@ export const ConnectorConfigurationForm = () => { }} component="form" > - {filteredConfigView.map((configEntry, index) => { + {localConfigView.map((configEntry, index) => { const { default_value: defaultValue, depends_on: dependencies, @@ -94,8 +76,8 @@ export const ConnectorConfigurationForm = () => { if (dependencies.length > 0) { // dynamic spacing without CSS - const previousField = filteredConfigView[index - 1]; - const nextField = filteredConfigView[index + 1]; + const previousField = localConfigView[index - 1]; + const nextField = localConfigView[index + 1]; const topSpacing = !previousField || previousField.depends_on.length <= 0 ? : <>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts index 64e7d1af9c999..f87d73b882ecd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts @@ -154,7 +154,7 @@ describe('ConnectorConfigurationLogic', () => { }); }); describe('setLocalConfigEntry', () => { - it('should set local config entry and sort keys', () => { + it('should set local config entry, and sort and filter keys', () => { ConnectorConfigurationLogic.actions.setConfigState({ bar: { default_value: '', @@ -182,6 +182,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }); ConnectorConfigurationLogic.actions.setLocalConfigState({ bar: { @@ -210,6 +281,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }); ConnectorConfigurationLogic.actions.setLocalConfigEntry({ default_value: '', @@ -254,6 +396,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }, configView: [ { @@ -284,6 +497,37 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + key: 'shownDependent1', + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + key: 'shownDependent2', + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, ], localConfigState: { bar: { @@ -312,6 +556,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }, localConfigView: [ { @@ -342,6 +657,37 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + key: 'hiddenDependent1', + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + key: 'hiddenDependent2', + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, ], }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts index 84b0fd4d23fdb..861ab90079229 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts @@ -72,12 +72,17 @@ export interface ConfigEntry { /** * - * Sorts the connector configuration by specified order (if present) + * Sorts and filters the connector configuration + * + * Sorting is done by specified order (if present) * otherwise by alphabetic order of keys * + * Filtering is done on any fields with ui_restrictions + * or that have not had their dependencies met + * */ -function sortConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry[] { - return Object.keys(config) +function sortAndFilterConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry[] { + const sortedConfig = Object.keys(config) .map( (key) => ({ @@ -98,6 +103,20 @@ function sortConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry } return a.key.localeCompare(b.key); }); + + const dependencyLookup: DependencyLookup = sortedConfig.reduce( + (prev: Record, configEntry: ConfigEntry) => ({ + ...prev, + [configEntry.key]: configEntry.value, + }), + {} + ); + + return sortedConfig.filter( + (configEntry) => + configEntry.ui_restrictions.length <= 0 && + dependenciesSatisfied(configEntry.depends_on, dependencyLookup) + ); } export function ensureStringType(value: string | number | boolean | null): string { @@ -280,11 +299,11 @@ export const ConnectorConfigurationLogic = kea< selectors: ({ selectors }) => ({ configView: [ () => [selectors.configState], - (configState: ConnectorConfiguration) => sortConnectorConfiguration(configState), + (configState: ConnectorConfiguration) => sortAndFilterConnectorConfiguration(configState), ], localConfigView: [ () => [selectors.localConfigState], - (configState) => sortConnectorConfiguration(configState), + (configState) => sortAndFilterConnectorConfiguration(configState), ], }), }); From 54457b074a20da8017de03feb9ebfbe0fe6450d3 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 24 Apr 2023 11:13:57 -0300 Subject: [PATCH 08/13] [Infrastructure UI] Plot metric charts data based on current page items (#155249) closes [#152186](https://github.com/elastic/kibana/issues/152186) ## Summary This PR makes the metric charts show data for the hosts on the current page. With this change, the charts will **only** load after the table has finished loading its data - or after Snapshot API has responded It also changes the current behavior of the table pagination and sorting. Instead of relying on the `EuiInMemoryTable` the pagination and sorting are done manually, and the EuiInMemoryTable has been replaced by the `EuiBasicTable`. The loading indicator has also been replaced. Paginating and sorting: https://user-images.githubusercontent.com/2767137/233161166-2bd719e1-7259-4ecc-96a7-50493bc6c0a3.mov Open in lens https://user-images.githubusercontent.com/2767137/233161134-621afd76-44b5-42ab-b58c-7f51ef944ac2.mov ### How to test - Go to Hosts view - Paginate and sort the table data - Select a page size and check if the select has been stored in the localStorage (`hostsView:pageSizeSelection` key) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../hosts/components/chart/chart_loader.tsx | 58 ++++++ .../hosts/components/chart/lens_wrapper.tsx | 87 ++++---- .../components/chart/metric_chart_wrapper.tsx | 53 ++--- .../metadata/metadata.test.tsx | 42 +--- .../hosts/components/hosts_container.tsx | 33 +-- .../metrics/hosts/components/hosts_table.tsx | 127 +++++------- .../hosts/components/kpis/kpi_grid.tsx | 5 +- .../metrics/hosts/components/kpis/tile.tsx | 15 +- .../components/tabs/logs/logs_tab_content.tsx | 5 +- .../components/tabs/metrics/metric_chart.tsx | 63 ++++-- .../public/pages/metrics/hosts/constants.ts | 3 + .../hosts/hooks/use_after_loaded_state.ts | 26 +++ .../metrics/hosts/hooks/use_alerts_query.ts | 2 +- .../hosts/hooks/use_hosts_table.test.ts | 192 +++++++++--------- .../metrics/hosts/hooks/use_hosts_table.tsx | 112 ++++++++-- .../hosts/hooks/use_hosts_table_url_state.ts | 94 +++++++++ .../hooks/use_table_properties_url_state.ts | 62 ------ .../infra/public/pages/metrics/hosts/utils.ts | 17 +- .../test/functional/apps/infra/hosts_view.ts | 83 ++++++++ .../page_objects/infra_hosts_view.ts | 47 +++++ 20 files changed, 716 insertions(+), 410 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts delete mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx new file mode 100644 index 0000000000000..bbddb338ef73f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiProgress, EuiFlexItem, EuiLoadingChart, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; + +export const ChartLoader = ({ + children, + loading, + style, + loadedOnce = false, + hasTitle = false, +}: { + style?: React.CSSProperties; + children: React.ReactNode; + loadedOnce: boolean; + loading: boolean; + hasTitle?: boolean; +}) => { + const { euiTheme } = useEuiTheme(); + return ( + + {loading && ( + + )} + {loading && !loadedOnce ? ( + + + + + + ) : ( + children + )} + + ); +}; + +const LoaderContainer = euiStyled.div` + position: relative; + border-radius: ${({ theme }) => theme.eui.euiSizeS}; + overflow: hidden; + height: 100%; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx index 9985db0751fd4..9a2472949f54c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx @@ -4,18 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiLoadingChart } from '@elastic/eui'; import { Filter, Query, TimeRange } from '@kbn/es-query'; import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; import { useIntersectedOnce } from '../../../../../hooks/use_intersection_once'; import { LensAttributes } from '../../../../../common/visualizations'; +import { ChartLoader } from './chart_loader'; export interface Props { id: string; @@ -26,7 +24,10 @@ export interface Props { extraActions: Action[]; lastReloadRequestTime?: number; style?: React.CSSProperties; + loading?: boolean; + hasTitle?: boolean; onBrushEnd?: (data: BrushTriggerEvent['data']) => void; + onLoad?: () => void; } export const LensWrapper = ({ @@ -39,12 +40,19 @@ export const LensWrapper = ({ style, onBrushEnd, lastReloadRequestTime, + loading = false, + hasTitle = false, }: Props) => { - const intersectionRef = React.useRef(null); + const intersectionRef = useRef(null); + const [loadedOnce, setLoadedOnce] = useState(false); + + const [state, setState] = useState({ + lastReloadRequestTime, + query, + filters, + dateRange, + }); - const [currentLastReloadRequestTime, setCurrentLastReloadRequestTime] = useState< - number | undefined - >(lastReloadRequestTime); const { services: { lens }, } = useKibanaContextForPlugin(); @@ -56,38 +64,49 @@ export const LensWrapper = ({ useEffect(() => { if ((intersection?.intersectionRatio ?? 0) === 1) { - setCurrentLastReloadRequestTime(lastReloadRequestTime); + setState({ + lastReloadRequestTime, + query, + dateRange, + filters, + }); } - }, [intersection?.intersectionRatio, lastReloadRequestTime]); + }, [dateRange, filters, intersection?.intersectionRatio, lastReloadRequestTime, query]); const isReady = attributes && intersectedOnce; return (
- {!isReady ? ( - - - - - - ) : ( - - )} + + {isReady && ( + { + if (!loadedOnce) { + setLoadedOnce(true); + } + }} + /> + )} +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx index 9df937983ae1e..8d78906bd03e9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx @@ -14,16 +14,11 @@ import { } from '@elastic/charts'; import { EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; -import { EuiLoadingChart } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; -import { EuiProgress } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { useEuiTheme } from '@elastic/eui'; import type { SnapshotNode, SnapshotNodeMetric } from '../../../../../../common/http_api'; import { createInventoryMetricFormatter } from '../../../inventory_view/lib/create_inventory_metric_formatter'; import type { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; +import { ChartLoader } from './chart_loader'; type MetricType = keyof Pick; @@ -65,7 +60,6 @@ export const MetricChartWrapper = ({ type, ...props }: Props) => { - const { euiTheme } = useEuiTheme(); const loadedOnce = useRef(false); const metrics = useMemo(() => (nodes ?? [])[0]?.metrics ?? [], [nodes]); const metricsTimeseries = useMemo( @@ -109,39 +103,18 @@ export const MetricChartWrapper = ({ return ( -
- {loading && ( - - )} - {loading && !loadedOnce.current ? ( - - - - - - ) : ( - - - - - - )} -
+ + + + + + +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx index 1c6320c142d7a..46392fa8609d1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx @@ -32,42 +32,12 @@ const metadataProps: TabProps = { name: 'host-1', cloudProvider: 'gcp', }, - rx: { - name: 'rx', - value: 0, - max: 0, - avg: 0, - }, - tx: { - name: 'tx', - value: 0, - max: 0, - avg: 0, - }, - memory: { - name: 'memory', - value: 0.5445920331099282, - max: 0.5445920331099282, - avg: 0.5445920331099282, - }, - cpu: { - name: 'cpu', - value: 0.2000718443867342, - max: 0.2000718443867342, - avg: 0.2000718443867342, - }, - diskLatency: { - name: 'diskLatency', - value: null, - max: 0, - avg: 0, - }, - memoryTotal: { - name: 'memoryTotal', - value: 16777216, - max: 16777216, - avg: 16777216, - }, + rx: 0, + tx: 0, + memory: 0.5445920331099282, + cpu: 0.2000718443867342, + diskLatency: 0, + memoryTotal: 16777216, }, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx index e8e8a8a8e7c4f..0c965feca8e9e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx @@ -12,10 +12,11 @@ import { InfraLoadingPanel } from '../../../../components/loading'; import { useMetricsDataViewContext } from '../hooks/use_data_view'; import { UnifiedSearchBar } from './unified_search_bar'; import { HostsTable } from './hosts_table'; -import { HostsViewProvider } from '../hooks/use_hosts_view'; +import { KPIGrid } from './kpis/kpi_grid'; import { Tabs } from './tabs/tabs'; import { AlertsQueryProvider } from '../hooks/use_alerts_query'; -import { KPIGrid } from './kpis/kpi_grid'; +import { HostsViewProvider } from '../hooks/use_hosts_view'; +import { HostsTableProvider } from '../hooks/use_hosts_table'; export const HostContainer = () => { const { dataView, loading, hasError } = useMetricsDataViewContext(); @@ -38,19 +39,21 @@ export const HostContainer = () => { - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index ca6f904ceea84..535afe8befff5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -5,93 +5,78 @@ * 2.0. */ -import React, { useCallback } from 'react'; -import { EuiInMemoryTable } from '@elastic/eui'; +import React from 'react'; +import { EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEqual } from 'lodash'; import { NoData } from '../../../../components/empty_states'; -import { InfraLoadingPanel } from '../../../../components/loading'; -import { useHostsTable } from '../hooks/use_hosts_table'; -import { useTableProperties } from '../hooks/use_table_properties_url_state'; +import { HostNodeRow, useHostsTableContext } from '../hooks/use_hosts_table'; import { useHostsViewContext } from '../hooks/use_hosts_view'; import { useUnifiedSearchContext } from '../hooks/use_unified_search'; import { Flyout } from './host_details_flyout/flyout'; +import { DEFAULT_PAGE_SIZE } from '../constants'; -export const HostsTable = () => { - const { hostNodes, loading } = useHostsViewContext(); - const { onSubmit, searchCriteria } = useUnifiedSearchContext(); - const [properties, setProperties] = useTableProperties(); - - const { columns, items, isFlyoutOpen, closeFlyout, clickedItem } = useHostsTable(hostNodes, { - time: searchCriteria.dateRange, - }); - - const noData = items.length === 0; - - const onTableChange = useCallback( - ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field, direction } = sort; - - const sorting = field && direction ? { field, direction } : true; - const pagination = pageIndex >= 0 && pageSize !== 0 ? { pageIndex, pageSize } : true; - - if (!isEqual(properties.sorting, sorting)) { - setProperties({ sorting }); - } - if (!isEqual(properties.pagination, pagination)) { - setProperties({ pagination }); - } - }, - [setProperties, properties.pagination, properties.sorting] - ); +const PAGE_SIZE_OPTIONS = [5, 10, 20]; - if (loading) { - return ( - - ); - } +export const HostsTable = () => { + const { loading } = useHostsViewContext(); + const { onSubmit } = useUnifiedSearchContext(); - if (noData) { - return ( - onSubmit()} - testString="noMetricsDataPrompt" - /> - ); - } + const { + columns, + items, + currentPage, + isFlyoutOpen, + closeFlyout, + clickedItem, + onTableChange, + pagination, + sorting, + } = useHostsTableContext(); return ( <> - onSubmit()} + testString="noMetricsDataPrompt" + /> + ) + } /> {isFlyoutOpen && clickedItem && } diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx index 968e7462b38f4..2dbd0c4324eca 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx @@ -6,11 +6,8 @@ */ import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; - +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { KPIChartProps, Tile } from './tile'; import { HostsTile } from './hosts_tile'; import { ChartBaseProps } from '../chart/metric_chart_wrapper'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx index 480e6c415dc45..a95f18b4a10ee 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx @@ -8,13 +8,16 @@ import React from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiI18n } from '@elastic/eui'; +import { + EuiIcon, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiI18n, + EuiToolTip, +} from '@elastic/eui'; import styled from 'styled-components'; -import { EuiToolTip } from '@elastic/eui'; import { useLensAttributes } from '../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index 0fad370960f22..d5cc0b0f021d7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -24,7 +24,10 @@ export const LogsTabContent = () => { const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]); const { hostNodes, loading } = useHostsViewContext(); - const hostsFilterQuery = useMemo(() => createHostsFilter(hostNodes), [hostNodes]); + const hostsFilterQuery = useMemo( + () => createHostsFilter(hostNodes.map((p) => p.name)), + [hostNodes] + ); const logsLinkToStreamQuery = useMemo(() => { const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx index 252bea5389e3a..28d07b94d9437 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx @@ -4,20 +4,28 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiI18n } from '@elastic/eui'; +import { + EuiIcon, + EuiPanel, + EuiI18n, + EuiFlexGroup, + EuiFlexItem, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; import { useLensAttributes } from '../../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; import { HostsLensLineChartFormulas } from '../../../../../../common/visualizations'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; +import { createHostsFilter } from '../../../utils'; +import { useHostsTableContext } from '../../../hooks/use_hosts_table'; import { LensWrapper } from '../../chart/lens_wrapper'; +import { useAfterLoadedState } from '../../../hooks/use_after_loaded_state'; export interface MetricChartProps { title: string; @@ -29,9 +37,18 @@ export interface MetricChartProps { const MIN_HEIGHT = 300; export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => { + const { euiTheme } = useEuiTheme(); const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest } = useHostsViewContext(); + const { baseRequest, loading } = useHostsViewContext(); + const { currentPage } = useHostsTableContext(); + + // prevents updates on requestTs and serchCriteria states from relaoding the chart + // we want it to reload only once the table has finished loading + const { afterLoadedState } = useAfterLoadedState(loading, { + lastReloadRequestTime: baseRequest.requestTs, + ...searchCriteria, + }); const { attributes, getExtraActions, error } = useLensAttributes({ type, @@ -43,11 +60,22 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => visualizationType: 'lineChart', }); - const filters = [...searchCriteria.filters, ...searchCriteria.panelFilters]; + const hostsFilterQuery = useMemo(() => { + return createHostsFilter( + currentPage.map((p) => p.name), + dataView + ); + }, [currentPage, dataView]); + + const filters = [ + ...afterLoadedState.filters, + ...afterLoadedState.panelFilters, + ...[hostsFilterQuery], + ]; const extraActionOptions = getExtraActions({ - timeRange: searchCriteria.dateRange, + timeRange: afterLoadedState.dateRange, filters, - query: searchCriteria.query, + query: afterLoadedState.query, }); const extraActions: Action[] = [extraActionOptions.openInLens]; @@ -69,12 +97,15 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => hasShadow={false} hasBorder paddingSize={error ? 'm' : 'none'} - style={{ minHeight: MIN_HEIGHT }} + css={css` + min-height: calc(${MIN_HEIGHT} + ${euiTheme.size.l}); + position: 'relative'; + `} data-test-subj={`hostsView-metricChart-${type}`} > {error ? ( attributes={attributes} style={{ height: MIN_HEIGHT }} extraActions={extraActions} - lastReloadRequestTime={baseRequest.requestTs} - dateRange={searchCriteria.dateRange} + lastReloadRequestTime={afterLoadedState.lastReloadRequestTime} + dateRange={afterLoadedState.dateRange} filters={filters} - query={searchCriteria.query} + query={afterLoadedState.query} onBrushEnd={handleBrushEnd} + loading={loading} + hasTitle /> )} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index 98aa8a145e3a0..b854120a86887 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -13,6 +13,9 @@ export const ALERT_STATUS_ALL = 'all'; export const TIMESTAMP_FIELD = '@timestamp'; export const DATA_VIEW_PREFIX = 'infra_metrics'; +export const DEFAULT_PAGE_SIZE = 10; +export const LOCAL_STORAGE_PAGE_SIZE_KEY = 'hostsView:pageSizeSelection'; + export const ALL_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_ALL, label: i18n.translate('xpack.infra.hostsViewPage.tabs.alerts.alertStatusFilter.showAll', { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts new file mode 100644 index 0000000000000..8c9a84d4402f8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef } from 'react'; + +export const useAfterLoadedState = (loading: boolean, state: T) => { + const ref = useRef(undefined); + const [internalState, setInternalState] = useState(state); + + if (!ref.current || loading !== ref.current) { + ref.current = loading; + } + + useEffect(() => { + if (!loading) { + setInternalState(state); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref.current]); + + return { afterLoadedState: internalState }; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts index 9877d61643721..7a895591d68c7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts @@ -69,7 +69,7 @@ const createAlertsEsQuery = ({ const alertStatusFilter = createAlertStatusFilter(status); const dateFilter = createDateFilter(dateRange); - const hostsFilter = createHostsFilter(hostNodes); + const hostsFilter = createHostsFilter(hostNodes.map((p) => p.name)); const filters = [alertStatusFilter, dateFilter, hostsFilter].filter(Boolean) as Filter[]; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts index 4ae8823adaf2e..a921a0daeb011 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts @@ -8,68 +8,92 @@ import { useHostsTable } from './use_hosts_table'; import { renderHook } from '@testing-library/react-hooks'; import { SnapshotNode } from '../../../../../common/http_api'; +import * as useUnifiedSearchHooks from './use_unified_search'; +import * as useHostsViewHooks from './use_hosts_view'; -describe('useHostTable hook', () => { - it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { - const nodes: SnapshotNode[] = [ +jest.mock('./use_unified_search'); +jest.mock('./use_hosts_view'); + +const mockUseUnifiedSearchContext = + useUnifiedSearchHooks.useUnifiedSearchContext as jest.MockedFunction< + typeof useUnifiedSearchHooks.useUnifiedSearchContext + >; +const mockUseHostsViewContext = useHostsViewHooks.useHostsViewContext as jest.MockedFunction< + typeof useHostsViewHooks.useHostsViewContext +>; + +const mockHostNode: SnapshotNode[] = [ + { + metrics: [ { - metrics: [ - { - name: 'rx', - avg: 252456.92916666667, - }, - { - name: 'tx', - avg: 252758.425, - }, - { - name: 'memory', - avg: 0.94525, - }, - { - name: 'cpu', - value: 0.6353277777777777, - }, - { - name: 'memoryTotal', - avg: 34359.738368, - }, - ], - path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], - name: 'host-0', + name: 'rx', + avg: 252456.92916666667, }, { - metrics: [ - { - name: 'rx', - avg: 95.86339715321859, - }, - { - name: 'tx', - avg: 110.38566859563191, - }, - { - name: 'memory', - avg: 0.5400000214576721, - }, - { - name: 'cpu', - value: 0.8647805555555556, - }, - { - name: 'memoryTotal', - avg: 9.194304, - }, - ], - path: [ - { value: 'host-1', label: 'host-1' }, - { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, - ], - name: 'host-1', + name: 'tx', + avg: 252758.425, }, - ]; + { + name: 'memory', + avg: 0.94525, + }, + { + name: 'cpu', + value: 0.6353277777777777, + }, + { + name: 'memoryTotal', + avg: 34359.738368, + }, + ], + path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], + name: 'host-0', + }, + { + metrics: [ + { + name: 'rx', + avg: 95.86339715321859, + }, + { + name: 'tx', + avg: 110.38566859563191, + }, + { + name: 'memory', + avg: 0.5400000214576721, + }, + { + name: 'cpu', + value: 0.8647805555555556, + }, + { + name: 'memoryTotal', + avg: 9.194304, + }, + ], + path: [ + { value: 'host-1', label: 'host-1' }, + { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, + ], + name: 'host-1', + }, +]; + +describe('useHostTable hook', () => { + beforeAll(() => { + mockUseUnifiedSearchContext.mockReturnValue({ + searchCriteria: { + dateRange: { from: 'now-15m', to: 'now' }, + }, + } as ReturnType); - const items = [ + mockUseHostsViewContext.mockReturnValue({ + hostNodes: mockHostNode, + } as ReturnType); + }); + it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { + const expected = [ { name: 'host-0', os: '-', @@ -79,27 +103,11 @@ describe('useHostTable hook', () => { cloudProvider: 'aws', name: 'host-0', }, - rx: { - name: 'rx', - avg: 252456.92916666667, - }, - tx: { - name: 'tx', - avg: 252758.425, - }, - memory: { - name: 'memory', - avg: 0.94525, - }, - cpu: { - name: 'cpu', - value: 0.6353277777777777, - }, - memoryTotal: { - name: 'memoryTotal', - - avg: 34359.738368, - }, + rx: 252456.92916666667, + tx: 252758.425, + memory: 0.94525, + cpu: 0.6353277777777777, + memoryTotal: 34359.738368, }, { name: 'host-1', @@ -110,32 +118,16 @@ describe('useHostTable hook', () => { cloudProvider: null, name: 'host-1', }, - rx: { - name: 'rx', - avg: 95.86339715321859, - }, - tx: { - name: 'tx', - avg: 110.38566859563191, - }, - memory: { - name: 'memory', - avg: 0.5400000214576721, - }, - cpu: { - name: 'cpu', - value: 0.8647805555555556, - }, - memoryTotal: { - name: 'memoryTotal', - avg: 9.194304, - }, + rx: 95.86339715321859, + tx: 110.38566859563191, + memory: 0.5400000214576721, + cpu: 0.8647805555555556, + memoryTotal: 9.194304, }, ]; - const time = { from: 'now-15m', to: 'now', interval: '>=1m' }; - const { result } = renderHook(() => useHostsTable(nodes, { time })); + const { result } = renderHook(() => useHostsTable()); - expect(result.current.items).toStrictEqual(items); + expect(result.current.items).toStrictEqual(expected); }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 44a492f314c1c..2d2d6c9d7f8e4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -8,8 +8,10 @@ import React, { useCallback, useMemo } from 'react'; import { EuiBasicTableColumn, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { TimeRange } from '@kbn/es-query'; - +import createContainer from 'constate'; +import { isEqual } from 'lodash'; +import { CriteriaWithPagination } from '@elastic/eui'; +import { isNumber } from 'lodash/fp'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter'; import { HostsTableEntryTitle } from '../components/hosts_table_entry_title'; @@ -19,6 +21,9 @@ import type { SnapshotMetricInput, } from '../../../../../common/http_api'; import { useHostFlyoutOpen } from './use_host_flyout_open_url_state'; +import { Sorting, useHostsTableProperties } from './use_hosts_table_url_state'; +import { useHostsViewContext } from './use_hosts_view'; +import { useUnifiedSearchContext } from './use_unified_search'; /** * Columns and items types @@ -27,7 +32,7 @@ export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider'; type HostMetric = 'cpu' | 'diskLatency' | 'rx' | 'tx' | 'memory' | 'memoryTotal'; -type HostMetrics = Record; +type HostMetrics = Record; export interface HostNodeRow extends HostMetrics { os?: string | null; @@ -38,10 +43,6 @@ export interface HostNodeRow extends HostMetrics { id: string; } -interface HostTableParams { - time: TimeRange; -} - /** * Helper functions */ @@ -60,12 +61,41 @@ const buildItemsList = (nodes: SnapshotNode[]) => { cloudProvider: path.at(-1)?.cloudProvider ?? null, }, ...metrics.reduce((data, metric) => { - data[metric.name as HostMetric] = metric; + data[metric.name as HostMetric] = metric.avg ?? metric.value; return data; }, {} as HostMetrics), })) as HostNodeRow[]; }; +const isTitleColumn = (cell: any): cell is HostNodeRow['title'] => { + return typeof cell === 'object' && cell && 'name' in cell; +}; + +const sortValues = (aValue: any, bValue: any, { direction }: Sorting) => { + if (typeof aValue === 'string' && typeof bValue === 'string') { + return direction === 'desc' ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue); + } + + if (isNumber(aValue) && isNumber(bValue)) { + return direction === 'desc' ? bValue - aValue : aValue - bValue; + } + + return 1; +}; + +const sortTableData = + ({ direction, field }: Sorting) => + (a: HostNodeRow, b: HostNodeRow) => { + const aValue = a[field as keyof HostNodeRow]; + const bValue = b[field as keyof HostNodeRow]; + + if (isTitleColumn(aValue) && isTitleColumn(bValue)) { + return sortValues(aValue.name, bValue.name, { direction, field }); + } + + return sortValues(aValue, bValue, { direction, field }); + }; + /** * Columns translations */ @@ -120,7 +150,10 @@ const toggleDialogActionLabel = i18n.translate( /** * Build a table columns and items starting from the snapshot nodes. */ -export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) => { +export const useHostsTable = () => { + const { hostNodes } = useHostsViewContext(); + const { searchCriteria } = useUnifiedSearchContext(); + const [{ pagination, sorting }, setProperties] = useHostsTableProperties(); const { services: { telemetry }, } = useKibanaContextForPlugin(); @@ -139,12 +172,38 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) [telemetry] ); - const items = useMemo(() => buildItemsList(nodes), [nodes]); + const onTableChange = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + const { index: pageIndex, size: pageSize } = page; + const { field, direction } = sort ?? {}; + + const currentSorting = { field: field as keyof HostNodeRow, direction }; + const currentPagination = { pageIndex, pageSize }; + + if (!isEqual(sorting, currentSorting)) { + setProperties({ sorting: currentSorting }); + } else if (!isEqual(pagination, currentPagination)) { + setProperties({ pagination: currentPagination }); + } + }, + [setProperties, pagination, sorting] + ); + + const items = useMemo(() => buildItemsList(hostNodes), [hostNodes]); const clickedItem = useMemo( () => items.find(({ id }) => id === hostFlyoutOpen.clickedItemId), [hostFlyoutOpen.clickedItemId, items] ); + const currentPage = useMemo(() => { + const { pageSize = 0, pageIndex = 0 } = pagination; + + const endIndex = (pageIndex + 1) * pageSize; + const startIndex = pageIndex * pageSize; + + return items.sort(sortTableData(sorting)).slice(startIndex, endIndex); + }, [items, pagination, sorting]); + const columns: Array> = useMemo( () => [ { @@ -183,7 +242,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) render: (title: HostNodeRow['title']) => ( reportHostEntryClick(title)} /> ), @@ -197,7 +256,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageCpuUsageLabel, - field: 'cpu.avg', + field: 'cpu', sortable: true, 'data-test-subj': 'hostsView-tableRow-cpuUsage', render: (avg: number) => formatMetric('cpu', avg), @@ -205,7 +264,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: diskLatencyLabel, - field: 'diskLatency.avg', + field: 'diskLatency', sortable: true, 'data-test-subj': 'hostsView-tableRow-diskLatency', render: (avg: number) => formatMetric('diskLatency', avg), @@ -213,7 +272,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageRXLabel, - field: 'rx.avg', + field: 'rx', sortable: true, 'data-test-subj': 'hostsView-tableRow-rx', render: (avg: number) => formatMetric('rx', avg), @@ -221,7 +280,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageTXLabel, - field: 'tx.avg', + field: 'tx', sortable: true, 'data-test-subj': 'hostsView-tableRow-tx', render: (avg: number) => formatMetric('tx', avg), @@ -229,7 +288,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageTotalMemoryLabel, - field: 'memoryTotal.avg', + field: 'memoryTotal', sortable: true, 'data-test-subj': 'hostsView-tableRow-memoryTotal', render: (avg: number) => formatMetric('memoryTotal', avg), @@ -237,21 +296,34 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageMemoryUsageLabel, - field: 'memory.avg', + field: 'memory', sortable: true, 'data-test-subj': 'hostsView-tableRow-memory', render: (avg: number) => formatMetric('memory', avg), align: 'right', }, ], - [hostFlyoutOpen.clickedItemId, reportHostEntryClick, setFlyoutClosed, setHostFlyoutOpen, time] + [ + hostFlyoutOpen.clickedItemId, + reportHostEntryClick, + searchCriteria.dateRange, + setFlyoutClosed, + setHostFlyoutOpen, + ] ); return { columns, - items, clickedItem, - isFlyoutOpen: !!hostFlyoutOpen.clickedItemId, + currentPage, closeFlyout, + items, + isFlyoutOpen: !!hostFlyoutOpen.clickedItemId, + onTableChange, + pagination, + sorting, }; }; + +export const HostsTable = createContainer(useHostsTable); +export const [HostsTableProvider, useHostsTableContext] = HostsTable; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts new file mode 100644 index 0000000000000..b4889d62f5878 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import deepEqual from 'fast-deep-equal'; +import { useReducer } from 'react'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { DEFAULT_PAGE_SIZE, LOCAL_STORAGE_PAGE_SIZE_KEY } from '../constants'; + +export const GET_DEFAULT_TABLE_PROPERTIES: TableProperties = { + sorting: { + direction: 'asc', + field: 'name', + }, + pagination: { + pageIndex: 0, + pageSize: DEFAULT_PAGE_SIZE, + }, +}; + +const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; + +const reducer = (prevState: TableProperties, params: Payload) => { + const payload = Object.fromEntries(Object.entries(params).filter(([_, v]) => !!v)); + + return { + ...prevState, + ...payload, + }; +}; + +export const useHostsTableProperties = (): [TableProperties, TablePropertiesUpdater] => { + const [localStoragePageSize, setLocalStoragePageSize] = useLocalStorage( + LOCAL_STORAGE_PAGE_SIZE_KEY, + DEFAULT_PAGE_SIZE + ); + + const [urlState, setUrlState] = useUrlState({ + defaultState: { + ...GET_DEFAULT_TABLE_PROPERTIES, + pagination: { + ...GET_DEFAULT_TABLE_PROPERTIES.pagination, + pageSize: localStoragePageSize, + }, + }, + + decodeUrlState, + encodeUrlState, + urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, + }); + + const [properties, setProperties] = useReducer(reducer, urlState); + if (!deepEqual(properties, urlState)) { + setUrlState(properties); + if (localStoragePageSize !== properties.pagination.pageSize) { + setLocalStoragePageSize(properties.pagination.pageSize); + } + } + + return [properties, setProperties]; +}; + +const PaginationRT = rt.partial({ pageIndex: rt.number, pageSize: rt.number }); +const SortingRT = rt.intersection([ + rt.type({ + field: rt.string, + }), + rt.partial({ direction: rt.union([rt.literal('asc'), rt.literal('desc')]) }), +]); + +const TableStateRT = rt.type({ + pagination: PaginationRT, + sorting: SortingRT, +}); + +export type TableState = rt.TypeOf; +export type Payload = Partial; +export type TablePropertiesUpdater = (params: Payload) => void; + +export type Sorting = rt.TypeOf; +type TableProperties = rt.TypeOf; + +const encodeUrlState = TableStateRT.encode; +const decodeUrlState = (value: unknown) => { + return pipe(TableStateRT.decode(value), fold(constant(undefined), identity)); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts deleted file mode 100644 index 980fdf19a684c..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { constant, identity } from 'fp-ts/lib/function'; -import { useUrlState } from '../../../../utils/use_url_state'; - -export const GET_DEFAULT_TABLE_PROPERTIES = { - sorting: true, - pagination: true, -}; -const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; - -type Action = rt.TypeOf; -type PropertiesUpdater = (newProps: Action) => void; - -export const useTableProperties = (): [TableProperties, PropertiesUpdater] => { - const [urlState, setUrlState] = useUrlState({ - defaultState: GET_DEFAULT_TABLE_PROPERTIES, - decodeUrlState, - encodeUrlState, - urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, - }); - - const setProperties = (newProps: Action) => setUrlState({ ...urlState, ...newProps }); - - return [urlState, setProperties]; -}; - -const PaginationRT = rt.union([ - rt.boolean, - rt.partial({ pageIndex: rt.number, pageSize: rt.number }), -]); -const SortingRT = rt.union([rt.boolean, rt.type({ field: rt.string, direction: rt.any })]); - -const SetSortingRT = rt.partial({ - sorting: SortingRT, -}); - -const SetPaginationRT = rt.partial({ - pagination: PaginationRT, -}); - -const ActionRT = rt.intersection([SetSortingRT, SetPaginationRT]); - -const TablePropertiesRT = rt.type({ - pagination: PaginationRT, - sorting: SortingRT, -}); - -type TableProperties = rt.TypeOf; - -const encodeUrlState = TablePropertiesRT.encode; -const decodeUrlState = (value: unknown) => { - return pipe(TablePropertiesRT.decode(value), fold(constant(undefined), identity)); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts index a04fdfa46b279..5da9d36b0f587 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts @@ -5,16 +5,23 @@ * 2.0. */ -import { Filter } from '@kbn/es-query'; -import { SnapshotNode } from '../../../../common/http_api'; +import { DataViewBase, Filter } from '@kbn/es-query'; -export const createHostsFilter = (hostNodes: SnapshotNode[]): Filter => { +export const createHostsFilter = (hostNames: string[], dataView?: DataViewBase): Filter => { return { query: { terms: { - 'host.name': hostNodes.map((p) => p.name), + 'host.name': hostNames, }, }, - meta: {}, + meta: dataView + ? { + value: hostNames.join(), + type: 'phrases', + params: hostNames, + index: dataView.id, + key: 'host.name', + } + : {}, }; }; diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 3cf0091c93bd4..e9000a9cf3e6d 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -529,6 +529,89 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Pagination and Sorting', () => { + beforeEach(async () => { + await pageObjects.infraHostsView.changePageSize(5); + }); + + it('should show 5 rows on the first page', async () => { + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row, position) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[position])); + }); + }); + + it('should paginate to the last page', async () => { + await pageObjects.infraHostsView.paginateTo(2); + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[5])); + }); + }); + + it('should show all hosts on the same page', async () => { + await pageObjects.infraHostsView.changePageSize(10); + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row, position) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[position])); + }); + }); + + it('should sort by Disk Latency asc', async () => { + await pageObjects.infraHostsView.sortByDiskLatency(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[0]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[1]); + }); + + it('should sort by Disk Latency desc', async () => { + await pageObjects.infraHostsView.sortByDiskLatency(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[1]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[0]); + }); + + it('should sort by Title asc', async () => { + await pageObjects.infraHostsView.sortByTitle(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[0]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[5]); + }); + + it('should sort by Title desc', async () => { + await pageObjects.infraHostsView.sortByTitle(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[5]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[0]); + }); + }); }); }); }; diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index ae0cc601f8cc7..6478d208226ad 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -241,6 +241,7 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { async typeInQueryBar(query: string) { const queryBar = await this.getQueryBar(); + await queryBar.clearValueWithKeyboard(); return queryBar.type(query); }, @@ -249,5 +250,51 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { await testSubjects.click('querySubmitButton'); }, + + // Pagination + getPageNumberButton(pageNumber: number) { + return testSubjects.find(`pagination-button-${pageNumber - 1}`); + }, + + getPageSizeSelector() { + return testSubjects.find('tablePaginationPopoverButton'); + }, + + getPageSizeOption(pageSize: number) { + return testSubjects.find(`tablePagination-${pageSize}-rows`); + }, + + async changePageSize(pageSize: number) { + const pageSizeSelector = await this.getPageSizeSelector(); + await pageSizeSelector.click(); + const pageSizeOption = await this.getPageSizeOption(pageSize); + await pageSizeOption.click(); + }, + + async paginateTo(pageNumber: number) { + const paginationButton = await this.getPageNumberButton(pageNumber); + await paginationButton.click(); + }, + + // Sorting + getDiskLatencyHeader() { + return testSubjects.find('tableHeaderCell_diskLatency_4'); + }, + + getTitleHeader() { + return testSubjects.find('tableHeaderCell_title_1'); + }, + + async sortByDiskLatency() { + const diskLatency = await this.getDiskLatencyHeader(); + const button = await testSubjects.findDescendant('tableHeaderSortButton', diskLatency); + return button.click(); + }, + + async sortByTitle() { + const titleHeader = await this.getTitleHeader(); + const button = await testSubjects.findDescendant('tableHeaderSortButton', titleHeader); + return button.click(); + }, }; } From 111d04f45a64cc050407bd9f892e1f77ddd8cc9f Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Mon, 24 Apr 2023 16:27:38 +0200 Subject: [PATCH 09/13] [APM] Add transaction name filter in failed transaction rate rule type (#155405) part of https://github.com/elastic/kibana/issues/152329 related work https://github.com/elastic/kibana/pull/154241 Introduces the Transaction name filter in the failed transaction rate rule type https://user-images.githubusercontent.com/3369346/233386404-1875b283-0321-4bf1-a7d3-66327f7d4ec5.mov ## Fixes The regression introduces in a previous [PR](https://github.com/elastic/kibana/pull/154241/commits/fce4ef8168429645a01434e19b0feaefba1a4f02) Existing rule types can have empty string in their params so we need to make sure we don't filter empty values as it will yield no results. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/common/rules/schema.ts | 1 + .../index.stories.tsx | 105 ++++++++++++++++++ .../index.tsx | 16 ++- .../register_error_count_rule_type.ts | 8 +- ...register_transaction_duration_rule_type.ts | 12 +- ...et_transaction_error_rate_chart_preview.ts | 13 ++- ...gister_transaction_error_rate_rule_type.ts | 16 ++- .../tests/alerts/chart_preview.spec.ts | 55 +++++++++ 8 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx diff --git a/x-pack/plugins/apm/common/rules/schema.ts b/x-pack/plugins/apm/common/rules/schema.ts index 698b4507c5b3f..ca77e76f6f156 100644 --- a/x-pack/plugins/apm/common/rules/schema.ts +++ b/x-pack/plugins/apm/common/rules/schema.ts @@ -52,6 +52,7 @@ export const transactionErrorRateParamsSchema = schema.object({ windowUnit: schema.string(), threshold: schema.number(), transactionType: schema.maybe(schema.string()), + transactionName: schema.maybe(schema.string()), serviceName: schema.maybe(schema.string()), environment: schema.string(), }); diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx new file mode 100644 index 0000000000000..cd94439db0389 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Story } from '@storybook/react'; +import React, { ComponentType, useState } from 'react'; +import { CoreStart } from '@kbn/core/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { RuleParams, TransactionErrorRateRuleType } from '.'; +import { AlertMetadata } from '../../utils/helper'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; + +const KibanaReactContext = createKibanaReactContext({ + notifications: { toasts: { add: () => {} } }, +} as unknown as Partial); + +interface Args { + ruleParams: RuleParams; + metadata?: AlertMetadata; +} + +export default { + title: 'alerting/TransactionErrorRateRuleType', + component: TransactionErrorRateRuleType, + decorators: [ + (StoryComponent: ComponentType) => { + return ( + +
+ +
+
+ ); + }, + ], +}; + +export const CreatingInApmServiceOverview: Story = ({ + ruleParams, + metadata, +}) => { + const [params, setParams] = useState(ruleParams); + + function setRuleParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; + +CreatingInApmServiceOverview.args = { + ruleParams: { + environment: 'testEnvironment', + serviceName: 'testServiceName', + threshold: 1500, + transactionType: 'testTransactionType', + transactionName: 'GET /api/customer/:id', + windowSize: 5, + windowUnit: 'm', + }, + metadata: { + environment: ENVIRONMENT_ALL.value, + serviceName: undefined, + }, +}; + +export const CreatingInStackManagement: Story = ({ + ruleParams, + metadata, +}) => { + const [params, setParams] = useState(ruleParams); + + function setRuleParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; + +CreatingInStackManagement.args = { + ruleParams: { + environment: 'testEnvironment', + threshold: 1500, + windowSize: 5, + windowUnit: 'm', + }, + metadata: undefined, +}; diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx index f9cfd6a511ef2..f161ef085b3ea 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx @@ -23,20 +23,22 @@ import { IsAboveField, ServiceField, TransactionTypeField, + TransactionNameField, } from '../../utils/fields'; import { AlertMetadata, getIntervalAndTimeRange } from '../../utils/helper'; import { ApmRuleParamsContainer } from '../../ui_components/apm_rule_params_container'; -interface RuleParams { +export interface RuleParams { windowSize?: number; windowUnit?: string; threshold?: number; serviceName?: string; transactionType?: string; + transactionName?: string; environment?: string; } -interface Props { +export interface Props { ruleParams: RuleParams; metadata?: AlertMetadata; setRuleParams: (key: string, value: any) => void; @@ -78,6 +80,7 @@ export function TransactionErrorRateRuleType(props: Props) { environment: params.environment, serviceName: params.serviceName, transactionType: params.transactionType, + transactionName: params.transactionName, interval, start, end, @@ -89,6 +92,7 @@ export function TransactionErrorRateRuleType(props: Props) { }, [ params.transactionType, + params.transactionName, params.environment, params.serviceName, params.windowSize, @@ -102,7 +106,8 @@ export function TransactionErrorRateRuleType(props: Props) { onChange={(value) => { if (value !== params.serviceName) { setRuleParams('serviceName', value); - setRuleParams('transactionType', ''); + setRuleParams('transactionType', undefined); + setRuleParams('transactionName', undefined); setRuleParams('environment', ENVIRONMENT_ALL.value); } }} @@ -117,6 +122,11 @@ export function TransactionErrorRateRuleType(props: Props) { onChange={(value) => setRuleParams('environment', value)} serviceName={params.serviceName} />, + setRuleParams('transactionName', value)} + serviceName={params.serviceName} + />, { - const { serviceName, environment, transactionType, interval, start, end } = - alertParams; + const { + serviceName, + environment, + transactionType, + interval, + start, + end, + transactionName, + } = alertParams; const searchAggregatedTransactions = await getSearchTransactionsEvents({ config, @@ -62,6 +70,7 @@ export async function getTransactionErrorRateChartPreview({ filter: [ ...termQuery(SERVICE_NAME, serviceName), ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions( diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 7ceaf8ca78048..26b5847a205f1 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -32,6 +32,7 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_TYPE, + TRANSACTION_NAME, } from '../../../../../common/es_fields/apm'; import { EventOutcome } from '../../../../../common/event_outcome'; import { @@ -86,6 +87,7 @@ export function registerTransactionErrorRateRuleType({ apmActionVariables.interval, apmActionVariables.reason, apmActionVariables.serviceName, + apmActionVariables.transactionName, apmActionVariables.threshold, apmActionVariables.transactionType, apmActionVariables.triggerValue, @@ -142,8 +144,15 @@ export function registerTransactionErrorRateRuleType({ ], }, }, - ...termQuery(SERVICE_NAME, ruleParams.serviceName), - ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType), + ...termQuery(SERVICE_NAME, ruleParams.serviceName, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_NAME, ruleParams.transactionName, { + queryEmptyString: false, + }), ...environmentQuery(ruleParams.environment), ], }, @@ -232,6 +241,7 @@ export function registerTransactionErrorRateRuleType({ serviceName, transactionType, environment, + ruleParams.transactionName, ] .filter((name) => name) .join('_'); @@ -255,6 +265,7 @@ export function registerTransactionErrorRateRuleType({ [SERVICE_NAME]: serviceName, ...getEnvironmentEsField(environment), [TRANSACTION_TYPE]: transactionType, + [TRANSACTION_NAME]: ruleParams.transactionName, [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: errorRate, [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, @@ -272,6 +283,7 @@ export function registerTransactionErrorRateRuleType({ serviceName, threshold: ruleParams.threshold, transactionType, + transactionName: ruleParams.transactionName, triggerValue: asDecimalOrInteger(errorRate), viewInAppUrl, }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts index 7ec09849b7ff2..f95bb8de59a89 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts @@ -83,6 +83,61 @@ export default function ApiTest({ getService }: FtrProviderContext) { ).to.equal(true); }); + it('transaction_error_rate with transaction name', async () => { + const options = { + params: { + query: { + start, + end, + serviceName: 'opbeans-java', + transactionName: 'APIRestController#product', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview[0]).to.eql({ + x: 1627974600000, + y: 1, + }); + }); + + it('transaction_error_rate with nonexistent transaction name', async () => { + const options = { + params: { + query: { + start, + end, + serviceName: 'opbeans-java', + transactionName: 'foo', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.every( + (item: { x: number; y: number | null }) => item.y === null + ) + ).to.equal(true); + }); + it('error_count (with data)', async () => { const options = getOptions(); options.params.query.transactionType = undefined; From 1095375fe39e960d0569d818191bef468c93a44b Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 24 Apr 2023 16:42:37 +0200 Subject: [PATCH 10/13] [Cases] Close FilePreview with Escape key. (#155592) Fixes #155036 ## Summary Allow users to close the file preview in cases by using the Escape key. (e2e coming in a different PR with other tests) --- .../components/files/file_preview.test.tsx | 20 +++++++++++++++++++ .../public/components/files/file_preview.tsx | 18 +++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx index b02df3a82228f..c1d7fe20bee48 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import type { AppMockRenderer } from '../../common/mock'; @@ -35,4 +36,23 @@ describe('FilePreview', () => { expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); }); + + it('pressing escape calls closePreview', async () => { + const closePreview = jest.fn(); + + appMockRender.render(); + + await waitFor(() => + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + id: basicFileMock.id, + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + }) + ); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + + userEvent.keyboard('{esc}'); + + await waitFor(() => expect(closePreview).toHaveBeenCalled()); + }); }); diff --git a/x-pack/plugins/cases/public/components/files/file_preview.tsx b/x-pack/plugins/cases/public/components/files/file_preview.tsx index 1bb91c5b53ff7..09cee1320ec2a 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.tsx @@ -4,12 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import type { FileJSON } from '@kbn/shared-ux-file-types'; -import { EuiOverlayMask, EuiFocusTrap, EuiImage } from '@elastic/eui'; +import { EuiOverlayMask, EuiFocusTrap, EuiImage, keys } from '@elastic/eui'; import { useFilesContext } from '@kbn/shared-ux-file-context'; import type { Owner } from '../../../common/constants/types'; @@ -36,6 +36,20 @@ export const FilePreview = ({ closePreview, selectedFile }: FilePreviewProps) => const { client: filesClient } = useFilesContext(); const { owner } = useCasesContext(); + useEffect(() => { + const keyboardListener = (event: KeyboardEvent) => { + if (event.key === keys.ESCAPE || event.code === 'Escape') { + closePreview(); + } + }; + + window.addEventListener('keyup', keyboardListener); + + return () => { + window.removeEventListener('keyup', keyboardListener); + }; + }, [closePreview]); + return ( From a03d20be039d1c449b2848f46463bc423b6f5183 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 24 Apr 2023 15:51:36 +0100 Subject: [PATCH 11/13] skip flaky suite (#154970) --- .../sections/alerts_table/bulk_actions/bulk_actions.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx index f9d209549da0c..23fac59fca208 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx @@ -691,7 +691,8 @@ describe('AlertsTable.BulkActions', () => { ).toBeTruthy(); }); - describe('and clear the selection is clicked', () => { + // FLAKY: https://github.com/elastic/kibana/issues/154970 + describe.skip('and clear the selection is clicked', () => { it('should turn off the toolbar', async () => { const props = { ...tablePropsWithBulkActions, From 2c14b584f8f736f65211b5f738f9e0d764681346 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 24 Apr 2023 15:55:40 +0100 Subject: [PATCH 12/13] skip flaky suite (#155222) --- x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index 92320dad62087..54b1baae454bd 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -209,7 +209,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); } - describe('explain log rate spikes', async function () { + // FLAKY: https://github.com/elastic/kibana/issues/155222 + describe.skip('explain log rate spikes', async function () { for (const testData of explainLogRateSpikesTestData) { describe(`with '${testData.sourceIndexOrSavedSearch}'`, function () { before(async () => { From 3d78370aa584e179ae9e9d30fabe080242812d22 Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Mon, 24 Apr 2023 11:02:43 -0400 Subject: [PATCH 13/13] Fix API links when generating API key snippet (#155435) Fixes the Search Applications API page to set an URL to the ES plugin rather than Enterprise Search URL. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../engine_connect/engine_api_integration.tsx | 19 ++++++++++--------- .../engine_connect/search_application_api.tsx | 14 +++++++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx index b61614838d7a1..2fe691e262b64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx @@ -12,23 +12,24 @@ import { useValues } from 'kea'; import { EuiCodeBlock, EuiSpacer, EuiText, EuiTabs, EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; - +import { useCloudDetails } from '../../../../shared/cloud_details/cloud_details'; import { EngineViewLogic } from '../engine_view_logic'; import { EngineApiLogic } from './engine_api_logic'; -const SearchUISnippet = (enterpriseSearchUrl: string, engineName: string, apiKey: string) => ` +import { elasticsearchUrl } from './search_application_api'; + +const SearchUISnippet = (esUrl: string, engineName: string, apiKey: string) => `6 import EnginesAPIConnector from "@elastic/search-ui-engines-connector"; const connector = new EnginesAPIConnector({ - host: "${enterpriseSearchUrl}", + host: "${esUrl}", engineName: "${engineName}", apiKey: "${apiKey || ''}" });`; -const cURLSnippet = (enterpriseSearchUrl: string, engineName: string, apiKey: string) => ` -curl --location --request GET '${enterpriseSearchUrl}/api/engines/${engineName}/_search' \\ +const cURLSnippet = (esUrl: string, engineName: string, apiKey: string) => ` +curl --location --request GET '${esUrl}/${engineName}/_search' \\ --header 'Authorization: apiKey ${apiKey || ''}' \\ --header 'Content-Type: application/json' \\ --data-raw '{ @@ -47,19 +48,19 @@ interface Tab { export const EngineApiIntegrationStage: React.FC = () => { const [selectedTab, setSelectedTab] = React.useState('curl'); const { engineName } = useValues(EngineViewLogic); - const enterpriseSearchUrl = getEnterpriseSearchUrl(); const { apiKey } = useValues(EngineApiLogic); + const cloudContext = useCloudDetails(); const Tabs: Record = { curl: { - code: cURLSnippet(enterpriseSearchUrl, engineName, apiKey), + code: cURLSnippet(elasticsearchUrl(cloudContext), engineName, apiKey), language: 'bash', title: i18n.translate('xpack.enterpriseSearch.content.engine.api.step3.curlTitle', { defaultMessage: 'cURL', }), }, searchui: { - code: SearchUISnippet(enterpriseSearchUrl, engineName, apiKey), + code: SearchUISnippet(elasticsearchUrl(cloudContext), engineName, apiKey), language: 'javascript', title: i18n.translate('xpack.enterpriseSearch.content.engine.api.step3.searchUITitle', { defaultMessage: 'Search UI', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx index 9d3c27895657f..6934de4051bdb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx @@ -23,9 +23,10 @@ import { i18n } from '@kbn/i18n'; import { ANALYTICS_PLUGIN } from '../../../../../../common/constants'; import { COLLECTION_INTEGRATE_PATH } from '../../../../analytics/routes'; +import { CloudDetails, useCloudDetails } from '../../../../shared/cloud_details/cloud_details'; +import { decodeCloudId } from '../../../../shared/decode_cloud_id/decode_cloud_id'; import { docLinks } from '../../../../shared/doc_links'; import { generateEncodedPath } from '../../../../shared/encode_path_params'; -import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; import { KibanaLogic } from '../../../../shared/kibana'; import { EngineViewLogic } from '../engine_view_logic'; @@ -34,12 +35,19 @@ import { EngineApiIntegrationStage } from './engine_api_integration'; import { EngineApiLogic } from './engine_api_logic'; import { GenerateEngineApiKeyModal } from './generate_engine_api_key_modal/generate_engine_api_key_modal'; +export const elasticsearchUrl = (cloudContext: CloudDetails): string => { + const defaultUrl = 'https://localhost:9200'; + const url = + (cloudContext.cloudId && decodeCloudId(cloudContext.cloudId)?.elasticsearchUrl) || defaultUrl; + return url; +}; + export const SearchApplicationAPI = () => { const { engineName } = useValues(EngineViewLogic); const { isGenerateModalOpen } = useValues(EngineApiLogic); const { openGenerateModal, closeGenerateModal } = useActions(EngineApiLogic); - const enterpriseSearchUrl = getEnterpriseSearchUrl(); const { navigateToUrl } = useValues(KibanaLogic); + const cloudContext = useCloudDetails(); const steps = [ { @@ -132,7 +140,7 @@ export const SearchApplicationAPI = () => { - {enterpriseSearchUrl} + {elasticsearchUrl(cloudContext)}