From 845cb0ec56c80b3817529734ff6c42d1e65dfa8e Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Thu, 20 Mar 2025 16:28:32 -0500 Subject: [PATCH 1/4] [AI4DSOC] Alert summary KQL bar --- .../common/components/search_bar/index.tsx | 12 +- .../search_bar/search_bar_section.test.tsx | 75 +++++++++ .../search_bar/search_bar_section.tsx | 75 +++++++++ .../search_bar/sources_filter_button.test.tsx | 120 +++++++++++++++ .../search_bar/sources_filter_button.tsx | 132 ++++++++++++++++ .../components/alert_summary/wrapper.test.tsx | 16 +- .../components/alert_summary/wrapper.tsx | 7 +- .../alert_summary/use_get_sources.test.ts | 144 ++++++++++++++++++ .../hooks/alert_summary/use_get_sources.ts | 73 +++++++++ .../public/detections/utils/filter.test.ts | 123 +++++++++++++++ .../public/detections/utils/filter.ts | 92 +++++++++++ 11 files changed, 862 insertions(+), 7 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/search_bar/index.tsx index fe3839031d680..ce305b6e30622 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -7,7 +7,7 @@ import { set } from '@kbn/safer-lodash-set/fp'; import { getOr } from 'lodash/fp'; -import React, { memo, useEffect, useCallback, useMemo } from 'react'; +import React, { memo, useCallback, useEffect, useMemo } from 'react'; import type { ConnectedProps } from 'react-redux'; import { connect, useDispatch } from 'react-redux'; import type { Dispatch } from 'redux'; @@ -16,14 +16,14 @@ import deepEqual from 'fast-deep-equal'; import type { Filter, Query, TimeRange } from '@kbn/es-query'; import type { FilterManager, SavedQuery } from '@kbn/data-plugin/public'; +import type { DataViewSpec } from '@kbn/data-views-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; import type { OnTimeChangeProps } from '@elastic/eui'; -import type { DataViewSpec } from '@kbn/data-views-plugin/public'; import { inputsActions } from '../../store/inputs'; import type { InputsRange } from '../../store/inputs/model'; import type { InputsModelId } from '../../store/inputs/constants'; -import type { State, inputsModel } from '../../store'; +import type { inputsModel, State } from '../../store'; import { formatDate } from '../super_date_picker'; import { endSelector, @@ -51,6 +51,10 @@ interface SiemSearchBarProps { dataTestSubj?: string; hideFilterBar?: boolean; hideQueryInput?: boolean; + /** + * Allows to hide the query menu button displayed to the left of the query input. + */ + hideQueryMenu?: boolean; } export const SearchBarComponent = memo( @@ -60,6 +64,7 @@ export const SearchBarComponent = memo( fromStr, hideFilterBar = false, hideQueryInput = false, + hideQueryMenu = false, id, isLoading = false, pollForSignalIndex, @@ -337,6 +342,7 @@ export const SearchBarComponent = memo( showFilterBar={!hideFilterBar} showDatePicker={true} showQueryInput={!hideQueryInput} + showQueryMenu={!hideQueryMenu} allowSavingQueries dataTestSubj={dataTestSubj} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx new file mode 100644 index 0000000000000..a1c4a81ae083d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { PackageListItem } from '@kbn/fleet-plugin/common'; +import { installationStatuses } from '@kbn/fleet-plugin/common/constants'; +import { + SEARCH_BAR_TEST_ID, + SearchBarSection, + SOURCE_BUTTON_LOADING_TEST_ID, +} from './search_bar_section'; +import { useFindRulesQuery } from '../../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub'; +import { SOURCE_BUTTON_TEST_ID } from './sources_filter_button'; +import { useKibana } from '../../../../common/lib/kibana'; +import { useSources } from '../../../hooks/alert_summary/use_get_sources'; + +jest.mock('../../../../common/components/search_bar', () => ({ + // The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables so we can't use SEARCH_BAR_TEST_ID + SiemSearchBar: () =>
, +})); +jest.mock('../../../../detection_engine/rule_management/api/hooks/use_find_rules_query'); +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../hooks/alert_summary/use_get_sources'); + +const dataView: DataView = createStubDataView({ spec: {} }); +const packages: PackageListItem[] = [ + { + id: 'splunk', + name: 'splunk', + status: installationStatuses.Installed, + title: 'Splunk', + version: '', + }, +]; + +describe('', () => { + it('should render all components', () => { + (useFindRulesQuery as jest.Mock).mockReturnValue({ + isLoading: false, + }); + (useSources as jest.Mock).mockReturnValue([]); + (useKibana as jest.Mock).mockReturnValue({ + services: { data: { query: { filterManager: jest.fn() } } }, + }); + + const { getByTestId, queryByTestId } = render( + + ); + + expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(SOURCE_BUTTON_LOADING_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(SOURCE_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should render a loading skeleton for the source button while fetching rules', () => { + (useFindRulesQuery as jest.Mock).mockReturnValue({ + isLoading: true, + }); + (useSources as jest.Mock).mockReturnValue([]); + + const { getByTestId, queryByTestId } = render( + + ); + + expect(getByTestId(SOURCE_BUTTON_LOADING_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(SOURCE_BUTTON_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx new file mode 100644 index 0000000000000..e9abc1ddf2130 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { EuiFlexGroup, EuiFlexItem, EuiSkeletonRectangle } from '@elastic/eui'; +import type { PackageListItem } from '@kbn/fleet-plugin/common'; +import { useSources } from '../../../hooks/alert_summary/use_get_sources'; +import { useFindRulesQuery } from '../../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; +import { SiemSearchBar } from '../../../../common/components/search_bar'; +import { SourceFilterButton } from './sources_filter_button'; +import { InputsModelId } from '../../../../common/store/inputs/constants'; + +export const SOURCE_BUTTON_LOADING_TEST_ID = 'alert-summary-source-button-loading'; +export const SEARCH_BAR_TEST_ID = 'alert-summary-search-bar'; + +const SOURCE_BUTTON_LOADING_WIDTH = '120px'; +const SOURCE_BUTTON_LOADING_HEIGHT = '40px'; + +export interface SearchBarSectionProps { + /** + * DataView created for the alert summary page + */ + dataView: DataView; + /** + * List of installed AI for SOC integrations + */ + packages: PackageListItem[]; +} + +/** + * KQL bar at the top of the alert summary page. + * The component leverages the Security Solution SiemSearchBar which has a lot of logic tied to url and redux to store its values. + * The component also has a filter button to the left of the KQL bar that allows user to select sources. + * A source is friendly UI representation of an integration. For the AI for SOC effort, each integration has one rule associated with. + * This means that deselecting a source is equivalent to filtering out by the rule for that integration. + */ +export const SearchBarSection = memo(({ dataView, packages }: SearchBarSectionProps) => { + const { data, isLoading } = useFindRulesQuery({}); + const sources = useSources({ packages, ruleResponse: data }); + + const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]); + + return ( + <> + + + + + + + + + + + + ); +}); + +SearchBarSection.displayName = 'SearchBarSection'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.test.tsx new file mode 100644 index 0000000000000..b1f3f79b5ae89 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.test.tsx @@ -0,0 +1,120 @@ +/* + * 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 { act, render } from '@testing-library/react'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + SOURCE_BUTTON_TEST_ID, + SourceFilterButton, + SOURCES_LIST_TEST_ID, +} from './sources_filter_button'; +import type { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; + +jest.mock('../../../../common/lib/kibana'); + +const sources: EuiSelectableOption[] = [ + { + 'data-test-subj': 'first', + checked: 'on', + key: 'firstKey', + label: 'firstLabel', + }, + { + 'data-test-subj': 'second', + key: 'secondKey', + label: 'secondLabel', + }, +]; + +describe('', () => { + it('should render the component', async () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { data: { query: { filterManager: jest.fn() } } }, + }); + + await act(async () => { + const { getByTestId } = render(); + + const button = getByTestId(SOURCE_BUTTON_TEST_ID); + expect(button).toBeInTheDocument(); + button.click(); + + await new Promise(process.nextTick); + + expect(getByTestId(SOURCES_LIST_TEST_ID)).toBeInTheDocument(); + + expect(getByTestId('first')).toHaveTextContent('firstLabel'); + expect(getByTestId('second')).toHaveTextContent('secondLabel'); + }); + }); + + it('should add a negated filter to filterManager', async () => { + const getFilters = jest.fn().mockReturnValue([]); + const setFilters = jest.fn(); + (useKibana as jest.Mock).mockReturnValue({ + services: { data: { query: { filterManager: { getFilters, setFilters } } } }, + }); + + await act(async () => { + const { getByTestId } = render(); + + getByTestId(SOURCE_BUTTON_TEST_ID).click(); + + await new Promise(process.nextTick); + + getByTestId('first').click(); + expect(setFilters).toHaveBeenCalledWith([ + { + meta: { + alias: null, + disabled: false, + index: undefined, + key: 'kibana.alert.rule.name', + negate: true, + params: { query: 'firstKey' }, + type: 'phrase', + }, + query: { match_phrase: { 'kibana.alert.rule.name': 'firstKey' } }, + }, + ]); + }); + }); + + it('should remove the negated filter from filterManager', async () => { + const getFilters = jest.fn().mockReturnValue([ + { + meta: { + alias: null, + disabled: false, + index: undefined, + key: 'kibana.alert.rule.name', + negate: true, + params: { query: 'secondKey' }, + type: 'phrase', + }, + query: { match_phrase: { 'kibana.alert.rule.name': 'secondKey' } }, + }, + ]); + const setFilters = jest.fn(); + (useKibana as jest.Mock).mockReturnValue({ + services: { data: { query: { filterManager: { getFilters, setFilters } } } }, + }); + + await act(async () => { + const { getByTestId } = render(); + + getByTestId(SOURCE_BUTTON_TEST_ID).click(); + + await new Promise(process.nextTick); + + // creates a new filter that + getByTestId('second').click(); + expect(setFilters).toHaveBeenCalledWith([]); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.tsx new file mode 100644 index 0000000000000..ae4652d44a2c6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useState } from 'react'; +import { css } from '@emotion/react'; +import { + EuiFilterButton, + EuiFilterGroup, + EuiPopover, + EuiSelectable, + useEuiTheme, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import type { EuiSelectableOnChangeEvent } from '@elastic/eui/src/components/selectable/selectable'; +import type { Filter } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { updateFiltersArray } from '../../../utils/filter'; +import { useKibana } from '../../../../common/lib/kibana'; + +export const SOURCE_BUTTON_TEST_ID = 'alert-summary-source-button'; +export const SOURCES_LIST_TEST_ID = 'alert-summary-sources-list'; + +const SOURCES_BUTTON = i18n.translate('xpack.securitySolution.alertSummary.sources.buttonLabel', { + defaultMessage: 'Sources', +}); + +export const FILTER_KEY = 'kibana.alert.rule.name'; + +export interface SourceFilterButtonProps { + /** + * List of sources the user can select or deselect + */ + sources: EuiSelectableOption[]; +} + +/** + * Filter button displayed next to the KQL bar at the top of the alert summary page. + * A source is friendly UI representation of an integration. For the AI for SOC effort, each integration has one rule associated with. + * This means that deselecting a source is equivalent to filtering out by the rule for that integration. + * The EuiFilterButton works as follow: + * - if a source is selected, this means that no filters live in filterManager + * - if a source is deselected, this means that we have a negated filter for that rule in filterManager + */ +export const SourceFilterButton = memo(({ sources }: SourceFilterButtonProps) => { + const { euiTheme } = useEuiTheme(); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const togglePopover = useCallback(() => setIsPopoverOpen((value) => !value), []); + + const { + data: { + query: { filterManager }, + }, + } = useKibana().services; + + const filterGroupPopoverId = useGeneratedHtmlId({ + prefix: 'filterGroupPopover', + }); + + const [items, setItems] = useState(sources); + + const onChange = useCallback( + ( + options: EuiSelectableOption[], + _: EuiSelectableOnChangeEvent, + changedOption: EuiSelectableOption + ) => { + setItems(options); + + const ruleName = changedOption.key; + if (ruleName) { + const existingFilters = filterManager.getFilters(); + const newFilters: Filter[] = updateFiltersArray( + existingFilters, + FILTER_KEY, + ruleName, + changedOption.checked === 'on' + ); + filterManager.setFilters(newFilters); + } + }, + [filterManager] + ); + + const button = ( + item.checked === 'on')} + iconType="arrowDown" + isSelected={isPopoverOpen} + numActiveFilters={items.filter((item) => item.checked === 'on').length} + numFilters={items.filter((item) => item.checked !== 'off').length} + onClick={togglePopover} + > + {SOURCES_BUTTON} + + ); + + return ( + + + + {(list) => list} + + + + ); +}); + +SourceFilterButton.displayName = 'SourceFilterButton'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx index 2df9978cee082..3cbf144854c8c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx @@ -17,7 +17,13 @@ import { Wrapper, } from './wrapper'; import { useKibana } from '../../../common/lib/kibana'; +import { TestProviders } from '../../../common/mock'; +import { SEARCH_BAR_TEST_ID } from './search_bar/search_bar_section'; +jest.mock('../../../common/components/search_bar', () => ({ + // The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables so we can't use SEARCH_BAR_TEST_ID + SiemSearchBar: () =>
, +})); jest.mock('../../../common/lib/kibana'); const packages: PackageListItem[] = [ @@ -83,9 +89,10 @@ describe('', () => { services: { data: { dataViews: { - create: jest.fn().mockReturnValue({ id: 'id' }), + create: jest.fn().mockReturnValue({ id: 'id', toSpec: jest.fn() }), clearInstanceCache: jest.fn(), }, + query: { filterManager: { getFilters: jest.fn() } }, }, }, }); @@ -96,12 +103,17 @@ describe('', () => { })); await act(async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); await new Promise(process.nextTick); expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument(); expect(getByTestId(CONTENT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument(); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx index d12edd1f4c560..c16fa7288d9cf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx @@ -17,6 +17,7 @@ import { i18n } from '@kbn/i18n'; import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import { useKibana } from '../../../common/lib/kibana'; +import { SearchBarSection } from './search_bar/search_bar_section'; const DATAVIEW_ERROR = i18n.translate('xpack.securitySolution.alertSummary.dataViewError', { defaultMessage: 'Unable to create data view', @@ -31,7 +32,7 @@ const dataViewSpec: DataViewSpec = { title: '.alerts-security.alerts-default' }; export interface WrapperProps { /** - * List of installed Ai for SOC integrations + * List of installed AI for SOC integrations */ packages: PackageListItem[]; } @@ -89,7 +90,9 @@ export const Wrapper = memo(({ packages }: WrapperProps) => { title={

{DATAVIEW_ERROR}

} /> ) : ( -
{'wrapper'}
+
+ +
)} } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.test.ts new file mode 100644 index 0000000000000..a16a0bf5bdc28 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.test.ts @@ -0,0 +1,144 @@ +/* + * 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'; +import { useSources } from './use_get_sources'; +import { useKibana } from '../../../common/lib/kibana'; +import type { PackageListItem } from '@kbn/fleet-plugin/common'; +import { installationStatuses } from '@kbn/fleet-plugin/common/constants'; +import type { RulesQueryResponse } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; +import { FILTER_KEY } from '../../components/alert_summary/search_bar/sources_filter_button'; + +jest.mock('../../../common/lib/kibana'); + +describe('useSources', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return a checked source', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + query: { + filterManager: { + getFilters: jest.fn().mockReturnValue([]), + }, + }, + }, + }, + }); + + const packages: PackageListItem[] = [ + { + id: 'splunk', + name: 'splunk', + status: installationStatuses.Installed, + title: 'Splunk', + version: '', + }, + ]; + const ruleResponse = { + rules: [ + { + related_integrations: [{ package: 'splunk' }], + name: 'SplunkRuleName', + }, + ], + total: 0, + } as unknown as RulesQueryResponse; + + const { result } = renderHook(() => useSources({ packages, ruleResponse })); + + expect(result.current).toEqual([ + { + checked: 'on', + 'data-test-subj': 'alert-summary-source-option-Splunk', + key: 'SplunkRuleName', + label: 'Splunk', + }, + ]); + }); + + it('should return an un-checked source', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + query: { + filterManager: { + getFilters: jest.fn().mockReturnValue([ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'phrase', + key: FILTER_KEY, + params: { query: 'Splunk' }, + }, + query: { match_phrase: { [FILTER_KEY]: 'Splunk' } }, + }, + ]), + }, + }, + }, + }, + }); + + const packages: PackageListItem[] = [ + { + id: 'splunk', + name: 'splunk', + status: installationStatuses.Installed, + title: 'Splunk', + version: '', + }, + ]; + const ruleResponse = { + rules: [ + { + related_integrations: [{ package: 'splunk' }], + name: 'SplunkRuleName', + }, + ], + total: 0, + } as unknown as RulesQueryResponse; + + const { result } = renderHook(() => useSources({ packages, ruleResponse })); + + expect(result.current).toEqual([ + { + 'data-test-subj': 'alert-summary-source-option-Splunk', + key: 'SplunkRuleName', + label: 'Splunk', + }, + ]); + }); + + it('should not return a source if no rule match', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { query: { filterManager: { getFilters: jest.fn().mockReturnValue([]) } } }, + }, + }); + + const packages: PackageListItem[] = [ + { + id: 'splunk', + name: 'splunk', + status: installationStatuses.Installed, + title: 'Splunk', + version: '', + }, + ]; + const ruleResponse = undefined; + + const { result } = renderHook(() => useSources({ packages, ruleResponse })); + + expect(result.current).toHaveLength(0); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.ts new file mode 100644 index 0000000000000..49e71f864c90d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.ts @@ -0,0 +1,73 @@ +/* + * 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 { useMemo } from 'react'; +import type { PackageListItem } from '@kbn/fleet-plugin/common'; +import type { + EuiSelectableOption, + EuiSelectableOptionCheckedType, +} from '@elastic/eui/src/components/selectable/selectable_option'; +import type { RulesQueryResponse } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; +import { filterExistsInFiltersArray } from '../../utils/filter'; +import { useKibana } from '../../../common/lib/kibana'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import { FILTER_KEY } from '../../components/alert_summary/search_bar/sources_filter_button'; + +export const SOURCE_OPTION_TEST_ID = 'alert-summary-source-option-'; + +export interface UseSourcesParams { + /** + * List of installed AI for SOC integrations + */ + packages: PackageListItem[]; + /** + * All rules + */ + ruleResponse: RulesQueryResponse | undefined; +} + +/** + * Combining installed packages and rules to create an interface that the SourceFilterButton can take as input (as EuiSelectableOption). + * If there is not match between a package and the rules, the source is not returned. + * If a filter exists (we assume that this filter is negated) we do not mark the source as checked for the EuiFilterButton. + */ +export const useSources = ({ packages, ruleResponse }: UseSourcesParams): EuiSelectableOption[] => { + const { + data: { + query: { filterManager }, + }, + } = useKibana().services; + + // There can be existing filters coming from the url + const currentFilters = filterManager.getFilters(); + + return useMemo(() => { + const result: EuiSelectableOption[] = []; + + packages.forEach((p: PackageListItem) => { + const matchingRule = (ruleResponse?.rules || []).find((r: RuleResponse) => + r.related_integrations.map((ri) => ri.package).includes(p.name) + ); + + if (matchingRule) { + // Retrieves the filter from the key/value pair + const currentFilterExists = filterExistsInFiltersArray(currentFilters, FILTER_KEY, p.title); + + // A EuiSelectableOption is checked only if there is no matching filter for that rule + const source = { + 'data-test-subj': `${SOURCE_OPTION_TEST_ID}${p.title}`, + ...(!currentFilterExists && { checked: 'on' as EuiSelectableOptionCheckedType }), + key: matchingRule?.name, + label: p.title, + }; + result.push(source); + } + }); + + return result; + }, [currentFilters, packages, ruleResponse]); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.test.ts new file mode 100644 index 0000000000000..f34dc0b6902a6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Filter, PhraseFilter } from '@kbn/es-query'; +import { filterExistsInFiltersArray, FilterIn, FilterOut, updateFiltersArray } from './filter'; + +describe('filterExistsInFiltersArray', () => { + it('should return false if empty array', () => { + const existingFilters: PhraseFilter[] = []; + const key: string = 'key'; + const value: string = 'value'; + + const doesFilterExists = filterExistsInFiltersArray(existingFilters, key, value); + + expect(doesFilterExists).toBe(undefined); + }); + + it('should return false if wrong filter', () => { + const key: string = 'key'; + const value: string = 'value'; + const filter = { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'phrase', + key, + params: { query: value }, + }, + query: { match_phrase: { [key]: value } }, + }; + const existingFilters: PhraseFilter[] = [filter]; + const wrongKey: string = 'wrongKey'; + const wrongValue: string = 'wrongValue'; + + const doesFilterExists = filterExistsInFiltersArray(existingFilters, wrongKey, wrongValue); + + expect(doesFilterExists).toBe(undefined); + }); + + it('should return true', () => { + const key: string = 'key'; + const value: string = 'value'; + const filter = { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'phrase', + key, + params: { query: value }, + }, + query: { match_phrase: { [key]: value } }, + }; + const existingFilters: PhraseFilter[] = [filter]; + + const doesFilterExists = filterExistsInFiltersArray(existingFilters, key, value); + + expect(doesFilterExists).toBe(filter); + }); +}); + +describe('updateFiltersArray', () => { + it('should add new filter', () => { + const existingFilters: PhraseFilter[] = []; + const key: string = 'key'; + const value: string = 'value'; + const filterType: boolean = FilterOut; + + const newFilters = updateFiltersArray( + existingFilters, + key, + value, + filterType + ) as PhraseFilter[]; + + expect(newFilters).toEqual([ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'phrase', + key: 'key', + params: { query: 'value' }, + }, + query: { match_phrase: { key: 'value' } }, + }, + ]); + }); + + it(`should remove negated filter`, () => { + const key: string = 'key'; + const value: string = 'value'; + const existingFilters: Filter[] = [ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'phrase', + key, + params: { query: value }, + }, + query: { match_phrase: { [key]: value } }, + }, + ]; + const filterType: boolean = FilterIn; + + const newFilters = updateFiltersArray( + existingFilters, + key, + value, + filterType + ) as PhraseFilter[]; + + expect(newFilters).toHaveLength(0); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.ts new file mode 100644 index 0000000000000..a49c9f15e27ba --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Filter } from '@kbn/es-query'; + +export const FilterIn = true; +export const FilterOut = false; + +/** + * Creates a new filter to apply to the KQL bar. + * + * @param key A string value mainly representing the field of an indicator + * @param value A string value mainly representing the value of the indicator for the field + * @param negate Set to true when we create a negated filter (e.g. NOT threat.indicator.type: url) + * @returns The new {@link Filter} + */ +const createFilter = ({ + key, + value, + negate, +}: { + key: string; + value: string; + negate: boolean; + index?: string; +}): Filter => ({ + meta: { + alias: null, + negate, + disabled: false, + type: 'phrase', + key, + params: { query: value }, + }, + query: { match_phrase: { [key]: value } }, +}); + +/** + * Checks if the key/value pair already exists in an array of filters. + * + * @param filters Array of {@link Filter} retrieved from the SearchBar filterManager. + * @param key A string value mainly representing the field of an indicator + * @param value A string value mainly representing the value of the indicator for the field + * @returns The new {@link Filter} + */ +export const filterExistsInFiltersArray = ( + filters: Filter[], + key: string, + value: string +): Filter | undefined => + filters.find( + (f: Filter) => + f.meta.key === key && + typeof f.meta.params === 'object' && + 'query' in f.meta.params && + f.meta.params?.query === value + ); + +/** + * Takes an array of filters and returns the updated array according to: + * - if the filter already exists, we remove it + * - if the filter does not exist, we add it + * This assumes that the only filters that can exist are negated filters. + * + * @param existingFilters List of {@link Filter} retrieved from the filterManager + * @param key The value used in the newly created {@link Filter} as a key + * @param value The value used in the newly created {@link Filter} as a params query + * @param filterType Weather the function is called for a {@link FilterIn} or {@link FilterOut} action + * @returns the updated array of filters + */ +export const updateFiltersArray = ( + existingFilters: Filter[], + key: string, + value: string | null, + filterType: boolean +): Filter[] => { + const newFilter = createFilter({ key, value: value as string, negate: !filterType }); + + const filter: Filter | undefined = filterExistsInFiltersArray( + existingFilters, + key, + value as string + ); + + return filter != null + ? existingFilters.filter((f: Filter) => f !== filter) + : [...existingFilters, newFilter]; +}; From d339e01e7133799db77bf1600c14ca64d93cbb58 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Tue, 25 Mar 2025 14:16:13 -0500 Subject: [PATCH 2/4] move fetching rules within sources hook --- .../search_bar/search_bar_section.test.tsx | 14 +-- .../search_bar/search_bar_section.tsx | 6 +- ...et_sources.test.ts => use_sources.test.ts} | 118 ++++++++++++------ .../{use_get_sources.ts => use_sources.ts} | 36 ++++-- 4 files changed, 118 insertions(+), 56 deletions(-) rename x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/{use_get_sources.test.ts => use_sources.test.ts} (53%) rename x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/{use_get_sources.ts => use_sources.ts} (68%) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx index a1c4a81ae083d..657b47339d875 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx @@ -14,20 +14,18 @@ import { SearchBarSection, SOURCE_BUTTON_LOADING_TEST_ID, } from './search_bar_section'; -import { useFindRulesQuery } from '../../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; import type { DataView } from '@kbn/data-views-plugin/common'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub'; import { SOURCE_BUTTON_TEST_ID } from './sources_filter_button'; import { useKibana } from '../../../../common/lib/kibana'; -import { useSources } from '../../../hooks/alert_summary/use_get_sources'; +import { useSources } from '../../../hooks/alert_summary/use_sources'; jest.mock('../../../../common/components/search_bar', () => ({ // The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables so we can't use SEARCH_BAR_TEST_ID SiemSearchBar: () =>
, })); -jest.mock('../../../../detection_engine/rule_management/api/hooks/use_find_rules_query'); jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../hooks/alert_summary/use_get_sources'); +jest.mock('../../../hooks/alert_summary/use_sources'); const dataView: DataView = createStubDataView({ spec: {} }); const packages: PackageListItem[] = [ @@ -42,10 +40,10 @@ const packages: PackageListItem[] = [ describe('', () => { it('should render all components', () => { - (useFindRulesQuery as jest.Mock).mockReturnValue({ + (useSources as jest.Mock).mockReturnValue({ isLoading: false, + sources: [], }); - (useSources as jest.Mock).mockReturnValue([]); (useKibana as jest.Mock).mockReturnValue({ services: { data: { query: { filterManager: jest.fn() } } }, }); @@ -60,10 +58,10 @@ describe('', () => { }); it('should render a loading skeleton for the source button while fetching rules', () => { - (useFindRulesQuery as jest.Mock).mockReturnValue({ + (useSources as jest.Mock).mockReturnValue({ isLoading: true, + sources: [], }); - (useSources as jest.Mock).mockReturnValue([]); const { getByTestId, queryByTestId } = render( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx index e9abc1ddf2130..871f1ff6226df 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx @@ -9,8 +9,7 @@ import React, { memo, useMemo } from 'react'; import type { DataView } from '@kbn/data-views-plugin/common'; import { EuiFlexGroup, EuiFlexItem, EuiSkeletonRectangle } from '@elastic/eui'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; -import { useSources } from '../../../hooks/alert_summary/use_get_sources'; -import { useFindRulesQuery } from '../../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; +import { useSources } from '../../../hooks/alert_summary/use_sources'; import { SiemSearchBar } from '../../../../common/components/search_bar'; import { SourceFilterButton } from './sources_filter_button'; import { InputsModelId } from '../../../../common/store/inputs/constants'; @@ -40,8 +39,7 @@ export interface SearchBarSectionProps { * This means that deselecting a source is equivalent to filtering out by the rule for that integration. */ export const SearchBarSection = memo(({ dataView, packages }: SearchBarSectionProps) => { - const { data, isLoading } = useFindRulesQuery({}); - const sources = useSources({ packages, ruleResponse: data }); + const { isLoading, sources } = useSources({ packages }); const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.test.ts similarity index 53% rename from x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.test.ts rename to x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.test.ts index a16a0bf5bdc28..172c4611d3ba5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.test.ts @@ -6,14 +6,15 @@ */ import { renderHook } from '@testing-library/react'; -import { useSources } from './use_get_sources'; +import { useSources } from './use_sources'; import { useKibana } from '../../../common/lib/kibana'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import { installationStatuses } from '@kbn/fleet-plugin/common/constants'; -import type { RulesQueryResponse } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; +import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; import { FILTER_KEY } from '../../components/alert_summary/search_bar/sources_filter_button'; jest.mock('../../../common/lib/kibana'); +jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query'); describe('useSources', () => { beforeEach(() => { @@ -32,6 +33,18 @@ describe('useSources', () => { }, }, }); + (useFindRulesQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: { + rules: [ + { + related_integrations: [{ package: 'splunk' }], + name: 'SplunkRuleName', + }, + ], + total: 0, + }, + }); const packages: PackageListItem[] = [ { @@ -42,26 +55,20 @@ describe('useSources', () => { version: '', }, ]; - const ruleResponse = { - rules: [ + + const { result } = renderHook(() => useSources({ packages })); + + expect(result.current).toEqual({ + isLoading: false, + sources: [ { - related_integrations: [{ package: 'splunk' }], - name: 'SplunkRuleName', + checked: 'on', + 'data-test-subj': 'alert-summary-source-option-Splunk', + key: 'SplunkRuleName', + label: 'Splunk', }, ], - total: 0, - } as unknown as RulesQueryResponse; - - const { result } = renderHook(() => useSources({ packages, ruleResponse })); - - expect(result.current).toEqual([ - { - checked: 'on', - 'data-test-subj': 'alert-summary-source-option-Splunk', - key: 'SplunkRuleName', - label: 'Splunk', - }, - ]); + }); }); it('should return an un-checked source', () => { @@ -88,6 +95,18 @@ describe('useSources', () => { }, }, }); + (useFindRulesQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: { + rules: [ + { + related_integrations: [{ package: 'splunk' }], + name: 'SplunkRuleName', + }, + ], + total: 0, + }, + }); const packages: PackageListItem[] = [ { @@ -98,33 +117,60 @@ describe('useSources', () => { version: '', }, ]; - const ruleResponse = { - rules: [ + + const { result } = renderHook(() => useSources({ packages })); + + expect(result.current).toEqual({ + isLoading: false, + sources: [ { - related_integrations: [{ package: 'splunk' }], - name: 'SplunkRuleName', + 'data-test-subj': 'alert-summary-source-option-Splunk', + key: 'SplunkRuleName', + label: 'Splunk', }, ], - total: 0, - } as unknown as RulesQueryResponse; + }); + }); - const { result } = renderHook(() => useSources({ packages, ruleResponse })); + it('should not return a source if no rule match', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { query: { filterManager: { getFilters: jest.fn().mockReturnValue([]) } } }, + }, + }); + (useFindRulesQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: undefined, + }); - expect(result.current).toEqual([ + const packages: PackageListItem[] = [ { - 'data-test-subj': 'alert-summary-source-option-Splunk', - key: 'SplunkRuleName', - label: 'Splunk', + id: 'splunk', + name: 'splunk', + status: installationStatuses.Installed, + title: 'Splunk', + version: '', }, - ]); + ]; + + const { result } = renderHook(() => useSources({ packages })); + + expect(result.current).toEqual({ + isLoading: false, + sources: [], + }); }); - it('should not return a source if no rule match', () => { + it('should return isLoading true if rules are loading', () => { (useKibana as jest.Mock).mockReturnValue({ services: { data: { query: { filterManager: { getFilters: jest.fn().mockReturnValue([]) } } }, }, }); + (useFindRulesQuery as jest.Mock).mockReturnValue({ + isLoading: true, + data: undefined, + }); const packages: PackageListItem[] = [ { @@ -135,10 +181,12 @@ describe('useSources', () => { version: '', }, ]; - const ruleResponse = undefined; - const { result } = renderHook(() => useSources({ packages, ruleResponse })); + const { result } = renderHook(() => useSources({ packages })); - expect(result.current).toHaveLength(0); + expect(result.current).toEqual({ + isLoading: true, + sources: [], + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.ts similarity index 68% rename from x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.ts rename to x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.ts index 49e71f864c90d..734917faa9da0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_sources.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.ts @@ -11,7 +11,7 @@ import type { EuiSelectableOption, EuiSelectableOptionCheckedType, } from '@elastic/eui/src/components/selectable/selectable_option'; -import type { RulesQueryResponse } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; +import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; import { filterExistsInFiltersArray } from '../../utils/filter'; import { useKibana } from '../../../common/lib/kibana'; import type { RuleResponse } from '../../../../common/api/detection_engine'; @@ -24,32 +24,42 @@ export interface UseSourcesParams { * List of installed AI for SOC integrations */ packages: PackageListItem[]; +} + +export interface UseSourcesResult { + /** + * True while rules are being fetched + */ + isLoading: boolean; /** - * All rules + * List of sources ready to be consumed by the SourceFilterButton component */ - ruleResponse: RulesQueryResponse | undefined; + sources: EuiSelectableOption[]; } /** * Combining installed packages and rules to create an interface that the SourceFilterButton can take as input (as EuiSelectableOption). - * If there is not match between a package and the rules, the source is not returned. + * If there is no match between a package and the rules, the source is not returned. * If a filter exists (we assume that this filter is negated) we do not mark the source as checked for the EuiFilterButton. */ -export const useSources = ({ packages, ruleResponse }: UseSourcesParams): EuiSelectableOption[] => { +export const useSources = ({ packages }: UseSourcesParams): UseSourcesResult => { + // Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total) + const { data, isLoading } = useFindRulesQuery({}); + const { data: { query: { filterManager }, }, } = useKibana().services; - // There can be existing filters coming from the url + // There can be existing rules filtered out, coming when parsing the url const currentFilters = filterManager.getFilters(); - return useMemo(() => { + const sources = useMemo(() => { const result: EuiSelectableOption[] = []; packages.forEach((p: PackageListItem) => { - const matchingRule = (ruleResponse?.rules || []).find((r: RuleResponse) => + const matchingRule = (data?.rules || []).find((r: RuleResponse) => r.related_integrations.map((ri) => ri.package).includes(p.name) ); @@ -69,5 +79,13 @@ export const useSources = ({ packages, ruleResponse }: UseSourcesParams): EuiSel }); return result; - }, [currentFilters, packages, ruleResponse]); + }, [currentFilters, data, packages]); + + return useMemo( + () => ({ + isLoading, + sources, + }), + [isLoading, sources] + ); }; From 4d55975a867ca66d88cdb821076f9d6924c251c3 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Thu, 27 Mar 2025 20:10:07 -0500 Subject: [PATCH 3/4] rename source to integration for clarity + PR comments --- ...sx => integrations_filter_button.test.tsx} | 26 ++++---- ...ton.tsx => integrations_filter_button.tsx} | 39 ++++++------ .../search_bar/search_bar_section.test.tsx | 30 ++++----- .../search_bar/search_bar_section.tsx | 62 +++++++++---------- ...urces.test.ts => use_integrations.test.ts} | 32 +++++----- .../{use_sources.ts => use_integrations.ts} | 40 ++++++------ .../public/detections/utils/filter.ts | 2 +- 7 files changed, 114 insertions(+), 117 deletions(-) rename x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/{sources_filter_button.test.tsx => integrations_filter_button.test.tsx} (79%) rename x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/{sources_filter_button.tsx => integrations_filter_button.tsx} (71%) rename x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/{use_sources.test.ts => use_integrations.test.ts} (83%) rename x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/{use_sources.ts => use_integrations.ts} (67%) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/integrations_filter_button.test.tsx similarity index 79% rename from x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/integrations_filter_button.test.tsx index b1f3f79b5ae89..db6624a45baa0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/integrations_filter_button.test.tsx @@ -9,15 +9,15 @@ import React from 'react'; import { act, render } from '@testing-library/react'; import { useKibana } from '../../../../common/lib/kibana'; import { - SOURCE_BUTTON_TEST_ID, - SourceFilterButton, - SOURCES_LIST_TEST_ID, -} from './sources_filter_button'; + INTEGRATION_BUTTON_TEST_ID, + IntegrationFilterButton, + INTEGRATIONS_LIST_TEST_ID, +} from './integrations_filter_button'; import type { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; jest.mock('../../../../common/lib/kibana'); -const sources: EuiSelectableOption[] = [ +const integrations: EuiSelectableOption[] = [ { 'data-test-subj': 'first', checked: 'on', @@ -31,22 +31,22 @@ const sources: EuiSelectableOption[] = [ }, ]; -describe('', () => { +describe('', () => { it('should render the component', async () => { (useKibana as jest.Mock).mockReturnValue({ services: { data: { query: { filterManager: jest.fn() } } }, }); await act(async () => { - const { getByTestId } = render(); + const { getByTestId } = render(); - const button = getByTestId(SOURCE_BUTTON_TEST_ID); + const button = getByTestId(INTEGRATION_BUTTON_TEST_ID); expect(button).toBeInTheDocument(); button.click(); await new Promise(process.nextTick); - expect(getByTestId(SOURCES_LIST_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(INTEGRATIONS_LIST_TEST_ID)).toBeInTheDocument(); expect(getByTestId('first')).toHaveTextContent('firstLabel'); expect(getByTestId('second')).toHaveTextContent('secondLabel'); @@ -61,9 +61,9 @@ describe('', () => { }); await act(async () => { - const { getByTestId } = render(); + const { getByTestId } = render(); - getByTestId(SOURCE_BUTTON_TEST_ID).click(); + getByTestId(INTEGRATION_BUTTON_TEST_ID).click(); await new Promise(process.nextTick); @@ -106,9 +106,9 @@ describe('', () => { }); await act(async () => { - const { getByTestId } = render(); + const { getByTestId } = render(); - getByTestId(SOURCE_BUTTON_TEST_ID).click(); + getByTestId(INTEGRATION_BUTTON_TEST_ID).click(); await new Promise(process.nextTick); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/integrations_filter_button.tsx similarity index 71% rename from x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/integrations_filter_button.tsx index ae4652d44a2c6..be9803a6b107e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/sources_filter_button.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/integrations_filter_button.tsx @@ -22,31 +22,34 @@ import { i18n } from '@kbn/i18n'; import { updateFiltersArray } from '../../../utils/filter'; import { useKibana } from '../../../../common/lib/kibana'; -export const SOURCE_BUTTON_TEST_ID = 'alert-summary-source-button'; -export const SOURCES_LIST_TEST_ID = 'alert-summary-sources-list'; +export const INTEGRATION_BUTTON_TEST_ID = 'alert-summary-integration-button'; +export const INTEGRATIONS_LIST_TEST_ID = 'alert-summary-integrations-list'; -const SOURCES_BUTTON = i18n.translate('xpack.securitySolution.alertSummary.sources.buttonLabel', { - defaultMessage: 'Sources', -}); +const INTEGRATIONS_BUTTON = i18n.translate( + 'xpack.securitySolution.alertSummary.integrations.buttonLabel', + { + defaultMessage: 'Integrations', + } +); export const FILTER_KEY = 'kibana.alert.rule.name'; -export interface SourceFilterButtonProps { +export interface IntegrationFilterButtonProps { /** - * List of sources the user can select or deselect + * List of integrations the user can select or deselect */ - sources: EuiSelectableOption[]; + integrations: EuiSelectableOption[]; } /** * Filter button displayed next to the KQL bar at the top of the alert summary page. - * A source is friendly UI representation of an integration. For the AI for SOC effort, each integration has one rule associated with. - * This means that deselecting a source is equivalent to filtering out by the rule for that integration. + * For the AI for SOC effort, each integration has one rule associated with. + * This means that deselecting an integration is equivalent to filtering out by the rule for that integration. * The EuiFilterButton works as follow: - * - if a source is selected, this means that no filters live in filterManager - * - if a source is deselected, this means that we have a negated filter for that rule in filterManager + * - if an integration is selected, this means that no filters live in filterManager + * - if an integration is deselected, this means that we have a negated filter for that rule in filterManager */ -export const SourceFilterButton = memo(({ sources }: SourceFilterButtonProps) => { +export const IntegrationFilterButton = memo(({ integrations }: IntegrationFilterButtonProps) => { const { euiTheme } = useEuiTheme(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -62,7 +65,7 @@ export const SourceFilterButton = memo(({ sources }: SourceFilterButtonProps) => prefix: 'filterGroupPopover', }); - const [items, setItems] = useState(sources); + const [items, setItems] = useState(integrations); const onChange = useCallback( ( @@ -93,7 +96,7 @@ export const SourceFilterButton = memo(({ sources }: SourceFilterButtonProps) => css={css` background-color: ${euiTheme.colors.backgroundBasePrimary}; `} - data-test-subj={SOURCE_BUTTON_TEST_ID} + data-test-subj={INTEGRATION_BUTTON_TEST_ID} hasActiveFilters={!!items.find((item) => item.checked === 'on')} iconType="arrowDown" isSelected={isPopoverOpen} @@ -101,7 +104,7 @@ export const SourceFilterButton = memo(({ sources }: SourceFilterButtonProps) => numFilters={items.filter((item) => item.checked !== 'off').length} onClick={togglePopover} > - {SOURCES_BUTTON} + {INTEGRATIONS_BUTTON} ); @@ -118,7 +121,7 @@ export const SourceFilterButton = memo(({ sources }: SourceFilterButtonProps) => css={css` min-width: 200px; `} - data-test-subj={SOURCES_LIST_TEST_ID} + data-test-subj={INTEGRATIONS_LIST_TEST_ID} options={items} onChange={onChange} > @@ -129,4 +132,4 @@ export const SourceFilterButton = memo(({ sources }: SourceFilterButtonProps) => ); }); -SourceFilterButton.displayName = 'SourceFilterButton'; +IntegrationFilterButton.displayName = 'IntegrationFilterButton'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx index 657b47339d875..f3b22e70c674b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx @@ -9,23 +9,19 @@ import React from 'react'; import { render } from '@testing-library/react'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import { installationStatuses } from '@kbn/fleet-plugin/common/constants'; -import { - SEARCH_BAR_TEST_ID, - SearchBarSection, - SOURCE_BUTTON_LOADING_TEST_ID, -} from './search_bar_section'; +import { INTEGRATION_BUTTON_LOADING_TEST_ID, SEARCH_BAR_TEST_ID, SearchBarSection } from './search_bar_section'; import type { DataView } from '@kbn/data-views-plugin/common'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub'; -import { SOURCE_BUTTON_TEST_ID } from './sources_filter_button'; +import { INTEGRATION_BUTTON_TEST_ID } from './integrations_filter_button'; import { useKibana } from '../../../../common/lib/kibana'; -import { useSources } from '../../../hooks/alert_summary/use_sources'; +import { useIntegrations } from '../../../hooks/alert_summary/use_integrations'; jest.mock('../../../../common/components/search_bar', () => ({ // The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables so we can't use SEARCH_BAR_TEST_ID SiemSearchBar: () =>
, })); jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../hooks/alert_summary/use_sources'); +jest.mock('../../../hooks/alert_summary/use_integrations'); const dataView: DataView = createStubDataView({ spec: {} }); const packages: PackageListItem[] = [ @@ -40,9 +36,9 @@ const packages: PackageListItem[] = [ describe('', () => { it('should render all components', () => { - (useSources as jest.Mock).mockReturnValue({ + (useIntegrations as jest.Mock).mockReturnValue({ isLoading: false, - sources: [], + integrations: [], }); (useKibana as jest.Mock).mockReturnValue({ services: { data: { query: { filterManager: jest.fn() } } }, @@ -53,21 +49,21 @@ describe('', () => { ); expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument(); - expect(queryByTestId(SOURCE_BUTTON_LOADING_TEST_ID)).not.toBeInTheDocument(); - expect(getByTestId(SOURCE_BUTTON_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(INTEGRATION_BUTTON_LOADING_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(INTEGRATION_BUTTON_TEST_ID)).toBeInTheDocument(); }); - it('should render a loading skeleton for the source button while fetching rules', () => { - (useSources as jest.Mock).mockReturnValue({ + it('should render a loading skeleton for the integration button while fetching rules', () => { + (useIntegrations as jest.Mock).mockReturnValue({ isLoading: true, - sources: [], + integrations: [], }); const { getByTestId, queryByTestId } = render( ); - expect(getByTestId(SOURCE_BUTTON_LOADING_TEST_ID)).toBeInTheDocument(); - expect(queryByTestId(SOURCE_BUTTON_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(INTEGRATION_BUTTON_LOADING_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(INTEGRATION_BUTTON_TEST_ID)).not.toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx index 871f1ff6226df..572565852b111 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx @@ -9,16 +9,16 @@ import React, { memo, useMemo } from 'react'; import type { DataView } from '@kbn/data-views-plugin/common'; import { EuiFlexGroup, EuiFlexItem, EuiSkeletonRectangle } from '@elastic/eui'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; -import { useSources } from '../../../hooks/alert_summary/use_sources'; +import { useIntegrations } from '../../../hooks/alert_summary/use_integrations'; import { SiemSearchBar } from '../../../../common/components/search_bar'; -import { SourceFilterButton } from './sources_filter_button'; +import { IntegrationFilterButton } from './integrations_filter_button'; import { InputsModelId } from '../../../../common/store/inputs/constants'; -export const SOURCE_BUTTON_LOADING_TEST_ID = 'alert-summary-source-button-loading'; +export const INTEGRATION_BUTTON_LOADING_TEST_ID = 'alert-summary-integration-button-loading'; export const SEARCH_BAR_TEST_ID = 'alert-summary-search-bar'; -const SOURCE_BUTTON_LOADING_WIDTH = '120px'; -const SOURCE_BUTTON_LOADING_HEIGHT = '40px'; +const INTEGRATION_BUTTON_LOADING_WIDTH = '120px'; +const INTEGRATION_BUTTON_LOADING_HEIGHT = '40px'; export interface SearchBarSectionProps { /** @@ -34,39 +34,37 @@ export interface SearchBarSectionProps { /** * KQL bar at the top of the alert summary page. * The component leverages the Security Solution SiemSearchBar which has a lot of logic tied to url and redux to store its values. - * The component also has a filter button to the left of the KQL bar that allows user to select sources. - * A source is friendly UI representation of an integration. For the AI for SOC effort, each integration has one rule associated with. - * This means that deselecting a source is equivalent to filtering out by the rule for that integration. + * The component also has a filter button to the left of the KQL bar that allows user to select integrations. + * For the AI for SOC effort, each integration has one rule associated with. + * This means that deselecting an integration is equivalent to filtering out by the rule for that integration. */ export const SearchBarSection = memo(({ dataView, packages }: SearchBarSectionProps) => { - const { isLoading, sources } = useSources({ packages }); + const { isLoading, integrations } = useIntegrations({ packages }); const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]); return ( - <> - - - - - - - - - - - + + + + + + + + + + ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts similarity index 83% rename from x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.test.ts rename to x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts index 172c4611d3ba5..03d50a569ae73 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts @@ -6,22 +6,22 @@ */ import { renderHook } from '@testing-library/react'; -import { useSources } from './use_sources'; +import { useIntegrations } from './use_integrations'; import { useKibana } from '../../../common/lib/kibana'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import { installationStatuses } from '@kbn/fleet-plugin/common/constants'; import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query'; -import { FILTER_KEY } from '../../components/alert_summary/search_bar/sources_filter_button'; +import { FILTER_KEY } from '../../components/alert_summary/search_bar/integrations_filter_button'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query'); -describe('useSources', () => { +describe('useIntegrations', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should return a checked source', () => { + it('should return a checked integration', () => { (useKibana as jest.Mock).mockReturnValue({ services: { data: { @@ -56,14 +56,14 @@ describe('useSources', () => { }, ]; - const { result } = renderHook(() => useSources({ packages })); + const { result } = renderHook(() => useIntegrations({ packages })); expect(result.current).toEqual({ isLoading: false, - sources: [ + integrations: [ { checked: 'on', - 'data-test-subj': 'alert-summary-source-option-Splunk', + 'data-test-subj': 'alert-summary-integration-option-Splunk', key: 'SplunkRuleName', label: 'Splunk', }, @@ -71,7 +71,7 @@ describe('useSources', () => { }); }); - it('should return an un-checked source', () => { + it('should return an un-checked integration', () => { (useKibana as jest.Mock).mockReturnValue({ services: { data: { @@ -118,13 +118,13 @@ describe('useSources', () => { }, ]; - const { result } = renderHook(() => useSources({ packages })); + const { result } = renderHook(() => useIntegrations({ packages })); expect(result.current).toEqual({ isLoading: false, - sources: [ + integrations: [ { - 'data-test-subj': 'alert-summary-source-option-Splunk', + 'data-test-subj': 'alert-summary-integration-option-Splunk', key: 'SplunkRuleName', label: 'Splunk', }, @@ -132,7 +132,7 @@ describe('useSources', () => { }); }); - it('should not return a source if no rule match', () => { + it('should not return a integration if no rule match', () => { (useKibana as jest.Mock).mockReturnValue({ services: { data: { query: { filterManager: { getFilters: jest.fn().mockReturnValue([]) } } }, @@ -153,11 +153,11 @@ describe('useSources', () => { }, ]; - const { result } = renderHook(() => useSources({ packages })); + const { result } = renderHook(() => useIntegrations({ packages })); expect(result.current).toEqual({ isLoading: false, - sources: [], + integrations: [], }); }); @@ -182,11 +182,11 @@ describe('useSources', () => { }, ]; - const { result } = renderHook(() => useSources({ packages })); + const { result } = renderHook(() => useIntegrations({ packages })); expect(result.current).toEqual({ isLoading: true, - sources: [], + integrations: [], }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts similarity index 67% rename from x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.ts rename to x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts index 734917faa9da0..20f498309d72d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_sources.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts @@ -15,34 +15,34 @@ import { useFindRulesQuery } from '../../../detection_engine/rule_management/api import { filterExistsInFiltersArray } from '../../utils/filter'; import { useKibana } from '../../../common/lib/kibana'; import type { RuleResponse } from '../../../../common/api/detection_engine'; -import { FILTER_KEY } from '../../components/alert_summary/search_bar/sources_filter_button'; +import { FILTER_KEY } from '../../components/alert_summary/search_bar/integrations_filter_button'; -export const SOURCE_OPTION_TEST_ID = 'alert-summary-source-option-'; +export const INTEGRATION_OPTION_TEST_ID = 'alert-summary-integration-option-'; -export interface UseSourcesParams { +export interface UseIntegrationsParams { /** * List of installed AI for SOC integrations */ packages: PackageListItem[]; } -export interface UseSourcesResult { +export interface UseIntegrationsResult { /** - * True while rules are being fetched + * List of integrations ready to be consumed by the IntegrationFilterButton component */ - isLoading: boolean; + integrations: EuiSelectableOption[]; /** - * List of sources ready to be consumed by the SourceFilterButton component + * True while rules are being fetched */ - sources: EuiSelectableOption[]; + isLoading: boolean; } /** - * Combining installed packages and rules to create an interface that the SourceFilterButton can take as input (as EuiSelectableOption). - * If there is no match between a package and the rules, the source is not returned. - * If a filter exists (we assume that this filter is negated) we do not mark the source as checked for the EuiFilterButton. + * Combining installed packages and rules to create an interface that the IntegrationFilterButton can take as input (as EuiSelectableOption). + * If there is no match between a package and the rules, the integration is not returned. + * If a filter exists (we assume that this filter is negated) we do not mark the integration as checked for the EuiFilterButton. */ -export const useSources = ({ packages }: UseSourcesParams): UseSourcesResult => { +export const useIntegrations = ({ packages }: UseIntegrationsParams): UseIntegrationsResult => { // Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total) const { data, isLoading } = useFindRulesQuery({}); @@ -55,7 +55,7 @@ export const useSources = ({ packages }: UseSourcesParams): UseSourcesResult => // There can be existing rules filtered out, coming when parsing the url const currentFilters = filterManager.getFilters(); - const sources = useMemo(() => { + const integrations = useMemo(() => { const result: EuiSelectableOption[] = []; packages.forEach((p: PackageListItem) => { @@ -65,16 +65,16 @@ export const useSources = ({ packages }: UseSourcesParams): UseSourcesResult => if (matchingRule) { // Retrieves the filter from the key/value pair - const currentFilterExists = filterExistsInFiltersArray(currentFilters, FILTER_KEY, p.title); + const currentFilter = filterExistsInFiltersArray(currentFilters, FILTER_KEY, p.title); // A EuiSelectableOption is checked only if there is no matching filter for that rule - const source = { - 'data-test-subj': `${SOURCE_OPTION_TEST_ID}${p.title}`, - ...(!currentFilterExists && { checked: 'on' as EuiSelectableOptionCheckedType }), + const integration = { + 'data-test-subj': `${INTEGRATION_OPTION_TEST_ID}${p.title}`, + ...(!currentFilter && { checked: 'on' as EuiSelectableOptionCheckedType }), key: matchingRule?.name, label: p.title, }; - result.push(source); + result.push(integration); } }); @@ -83,9 +83,9 @@ export const useSources = ({ packages }: UseSourcesParams): UseSourcesResult => return useMemo( () => ({ + integrations, isLoading, - sources, }), - [isLoading, sources] + [integrations, isLoading] ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.ts index a49c9f15e27ba..d93f9d94d6bad 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/filter.ts @@ -86,7 +86,7 @@ export const updateFiltersArray = ( value as string ); - return filter != null + return filter ? existingFilters.filter((f: Filter) => f !== filter) : [...existingFilters, newFilter]; }; From b6a4ae5d76990b1064f05da42180736bca5de935 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 28 Mar 2025 01:33:51 +0000 Subject: [PATCH 4/4] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../alert_summary/search_bar/search_bar_section.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx index f3b22e70c674b..cc0f4a97abd4c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx @@ -9,7 +9,11 @@ import React from 'react'; import { render } from '@testing-library/react'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import { installationStatuses } from '@kbn/fleet-plugin/common/constants'; -import { INTEGRATION_BUTTON_LOADING_TEST_ID, SEARCH_BAR_TEST_ID, SearchBarSection } from './search_bar_section'; +import { + INTEGRATION_BUTTON_LOADING_TEST_ID, + SEARCH_BAR_TEST_ID, + SearchBarSection, +} from './search_bar_section'; import type { DataView } from '@kbn/data-views-plugin/common'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub'; import { INTEGRATION_BUTTON_TEST_ID } from './integrations_filter_button';