From d6fa87e0d5ef2bcb30a1b5474880f55ba492394a Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 2 May 2024 17:46:12 -0400 Subject: [PATCH] [8.14] [Security Solution] - Security solution ES|QL configurable via advanced setting (#181616) (#182517) # Backport This will backport the following commits from `main` to `8.14`: - [[Security Solution] - Security solution ES|QL configurable via advanced setting (#181616)](https://github.com/elastic/kibana/pull/181616) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- config/serverless.security.yml | 1 - .../common/config_settings.ts | 5 -- .../public/common/components/hooks/index.ts | 8 -- .../hooks/use_is_esql_rule_type_enabled.ts | 16 ---- .../hooks/esql/use_esql_availability.ts | 34 ++++++++ .../select_rule_type/index.test.tsx | 32 ++++++-- .../components/select_rule_type/index.tsx | 6 +- .../components/timeline/tabs/index.test.tsx | 82 +++++++++++++++++++ .../components/timeline/tabs/index.tsx | 20 +++-- .../public/timelines/store/selectors.ts | 8 ++ .../rule_preview/api/preview_rules/route.ts | 2 +- .../security_solution/server/plugin.ts | 2 +- .../rule_creation/esql_rule_serverless.cy.ts | 14 +--- 13 files changed, 174 insertions(+), 56 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/hooks/index.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/hooks/use_is_esql_rule_type_enabled.ts create mode 100644 x-pack/plugins/security_solution/public/common/hooks/esql/use_esql_availability.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx diff --git a/config/serverless.security.yml b/config/serverless.security.yml index 5ebb22486dc43..88770178a3493 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -23,7 +23,6 @@ xpack.securitySolutionServerless.productTypes: xpack.securitySolution.offeringSettings: { ILMEnabled: false, # Index Lifecycle Management (ILM) functionalities disabled, not supported by serverless Elasticsearch - ESQLEnabled: false, # ES|QL disabled, not supported by serverless Elasticsearch } newsfeed.enabled: true diff --git a/x-pack/plugins/security_solution/common/config_settings.ts b/x-pack/plugins/security_solution/common/config_settings.ts index 5d5c40fa2b48c..6dd9967f6e3fd 100644 --- a/x-pack/plugins/security_solution/common/config_settings.ts +++ b/x-pack/plugins/security_solution/common/config_settings.ts @@ -10,10 +10,6 @@ export interface ConfigSettings { * Index Lifecycle Management (ILM) feature enabled. */ ILMEnabled: boolean; - /** - * ESQL queries enabled. - */ - ESQLEnabled: boolean; } /** @@ -22,7 +18,6 @@ export interface ConfigSettings { */ export const defaultSettings: ConfigSettings = Object.freeze({ ILMEnabled: true, - ESQLEnabled: true, }); type ConfigSettingsKey = keyof ConfigSettings; diff --git a/x-pack/plugins/security_solution/public/common/components/hooks/index.ts b/x-pack/plugins/security_solution/public/common/components/hooks/index.ts deleted file mode 100644 index ba849f2c85864..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/hooks/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { useIsEsqlRuleTypeEnabled } from './use_is_esql_rule_type_enabled'; diff --git a/x-pack/plugins/security_solution/public/common/components/hooks/use_is_esql_rule_type_enabled.ts b/x-pack/plugins/security_solution/public/common/components/hooks/use_is_esql_rule_type_enabled.ts deleted file mode 100644 index 239c49088e644..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/hooks/use_is_esql_rule_type_enabled.ts +++ /dev/null @@ -1,16 +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 { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; -import { useKibana } from '../../lib/kibana'; - -export const useIsEsqlRuleTypeEnabled = (): boolean => { - const isEsqlSettingEnabled = useKibana().services.configSettings.ESQLEnabled; - const isEsqlRuleTypeEnabled = !useIsExperimentalFeatureEnabled('esqlRulesDisabled'); - - return isEsqlSettingEnabled && isEsqlRuleTypeEnabled; -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/esql/use_esql_availability.ts b/x-pack/plugins/security_solution/public/common/hooks/esql/use_esql_availability.ts new file mode 100644 index 0000000000000..41fc7084b32bf --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/esql/use_esql_availability.ts @@ -0,0 +1,34 @@ +/* + * 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 { ENABLE_ESQL } from '@kbn/esql-utils'; +import { useKibana } from '../../lib/kibana'; +import { useIsExperimentalFeatureEnabled } from '../use_experimental_features'; + +/** + * This hook combines the checks for esql availability within the security solution + * If the advanced setting is disabled, ESQL will not be accessible in the UI for any new timeline or new rule creation workflows + * The feature flags are still available to provide users an escape hatch in case of any esql related performance issues + */ +export const useEsqlAvailability = () => { + const { uiSettings } = useKibana().services; + const isEsqlAdvancedSettingEnabled = uiSettings?.get(ENABLE_ESQL); + const isEsqlRuleTypeEnabled = + !useIsExperimentalFeatureEnabled('esqlRulesDisabled') && isEsqlAdvancedSettingEnabled; + const isESQLTabInTimelineEnabled = + !useIsExperimentalFeatureEnabled('timelineEsqlTabDisabled') && isEsqlAdvancedSettingEnabled; + + return useMemo( + () => ({ + isEsqlAdvancedSettingEnabled, + isEsqlRuleTypeEnabled, + isESQLTabInTimelineEnabled, + }), + [isESQLTabInTimelineEnabled, isEsqlAdvancedSettingEnabled, isEsqlRuleTypeEnabled] + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.test.tsx index 73bdf48623e2a..9930dc7f626ad 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.test.tsx @@ -10,12 +10,12 @@ import { mount, shallow } from 'enzyme'; import { SelectRuleType } from '.'; import { TestProviders, useFormFieldMock } from '../../../../common/mock'; -import { useIsEsqlRuleTypeEnabled } from '../../../../common/components/hooks'; +import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; -jest.mock('../../../../common/components/hooks', () => ({ - useIsEsqlRuleTypeEnabled: jest.fn().mockReturnValue(true), +jest.mock('../../../../common/hooks/esql/use_esql_availability', () => ({ + useEsqlAvailability: jest.fn().mockReturnValue({ isEsqlRuleTypeEnabled: true }), })); -const useIsEsqlRuleTypeEnabledMock = useIsEsqlRuleTypeEnabled as jest.Mock; +const useEsqlAvailabilityMock = useEsqlAvailability as jest.Mock; describe('SelectRuleType', () => { it('renders correctly', () => { @@ -185,8 +185,30 @@ describe('SelectRuleType', () => { expect(wrapper.find('[data-test-subj="esqlRuleType"]').exists()).toBeTruthy(); }); + it('renders selected card only when in update mode for "esql" and esql feature is disabled', () => { + useEsqlAvailabilityMock.mockReturnValueOnce(false); + const field = useFormFieldMock({ value: 'esql' }); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="esqlRuleType"]').exists()).toBeTruthy(); + }); + it('should not render "esql" rule type if esql rule is not enabled', () => { - useIsEsqlRuleTypeEnabledMock.mockReturnValueOnce(false); + useEsqlAvailabilityMock.mockReturnValueOnce(false); const Component = () => { const field = useFormFieldMock(); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.tsx index c93b107849c2b..2b222fd3c393f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo, memo } from 'react'; import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { isThresholdRule, @@ -21,7 +22,6 @@ import { import type { FieldHook } from '../../../../shared_imports'; import * as i18n from './translations'; import { MlCardDescription } from './ml_card_description'; -import { useIsEsqlRuleTypeEnabled } from '../../../../common/components/hooks'; interface SelectRuleTypeProps { describedByIds: string[]; @@ -48,7 +48,7 @@ export const SelectRuleType: React.FC = memo( const setNewTerms = useCallback(() => setType('new_terms'), [setType]); const setEsql = useCallback(() => setType('esql'), [setType]); - const isEsqlRuleTypeEnabled = useIsEsqlRuleTypeEnabled(); + const { isEsqlRuleTypeEnabled } = useEsqlAvailability(); const eqlSelectableConfig = useMemo( () => ({ @@ -194,7 +194,7 @@ export const SelectRuleType: React.FC = memo( /> )} - {isEsqlRuleTypeEnabled && (!isUpdateView || esqlSelectableConfig.isSelected) && ( + {((!isUpdateView && isEsqlRuleTypeEnabled) || esqlSelectableConfig.isSelected) && ( ({ + useEsqlAvailability: jest.fn().mockReturnValue({ + isESQLTabInTimelineEnabled: true, + }), +})); + +const useEsqlAvailabilityMock = useEsqlAvailability as jest.Mock; + +describe('Timeline', () => { + describe('esql tab', () => { + const esqlTabSubj = `timelineTabs-${TimelineTabs.esql}`; + const defaultProps = { + renderCellValue: () => {}, + rowRenderers: [], + timelineId: TimelineId.test, + timelineType: TimelineType.default, + timelineDescription: '', + }; + + it('should show the esql tab', () => { + render( + + + + ); + expect(screen.getByTestId(esqlTabSubj)).toBeVisible(); + }); + + it('should not show the esql tab when the advanced setting is disabled', async () => { + useEsqlAvailabilityMock.mockReturnValue({ + isESQLTabInTimelineEnabled: false, + }); + render( + + + + ); + + await waitFor(() => { + expect(screen.queryByTestId(esqlTabSubj)).toBeNull(); + }); + }); + + it('should show the esql tab when the advanced setting is disabled, but an esql query is present', async () => { + useEsqlAvailabilityMock.mockReturnValue({ + isESQLTabInTimelineEnabled: false, + }); + + const stateWithSavedSearchId = structuredClone(mockGlobalState); + stateWithSavedSearchId.timeline.timelineById[TimelineId.test].savedSearchId = 'test-id'; + const mockStore = createMockStore(stateWithSavedSearchId); + + render( + + + + ); + + await waitFor(() => { + expect(screen.queryByTestId(esqlTabSubj)).toBeVisible(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx index 8c9d2dc1298e5..2e164677735dd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx @@ -13,8 +13,8 @@ import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { useKibana } from '../../../../common/lib/kibana'; import { useAssistantTelemetry } from '../../../../assistant/use_assistant_telemetry'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import type { SessionViewConfig } from '../../../../../common/types'; @@ -43,6 +43,7 @@ import * as i18n from './translations'; import { useLicense } from '../../../../common/hooks/use_license'; import { TIMELINE_CONVERSATION_TITLE } from '../../../../assistant/content/conversations/translations'; import { initializeTimelineSettings } from '../../../store/actions'; +import { selectTimelineESQLSavedSearchId } from '../../../store/selectors'; const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScroll: boolean }>( ({ $isVisible = false, isOverflowYScroll = false }) => ({ @@ -109,7 +110,11 @@ const ActiveTimelineTab = memo( showTimeline, }) => { const { hasAssistantPrivilege } = useAssistantAvailability(); - const isEsqlSettingEnabled = useKibana().services.configSettings.ESQLEnabled; + const { isESQLTabInTimelineEnabled } = useEsqlAvailability(); + const timelineESQLSavedSearch = useShallowEqualSelector((state) => + selectTimelineESQLSavedSearchId(state, timelineId) + ); + const shouldShowESQLTab = isESQLTabInTimelineEnabled || timelineESQLSavedSearch != null; const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); const getTab = useCallback( (tab: TimelineTabs) => { @@ -177,7 +182,7 @@ const ActiveTimelineTab = memo( timelineId={timelineId} /> - {showTimeline && isEsqlSettingEnabled && activeTimelineTab === TimelineTabs.esql && ( + {showTimeline && shouldShowESQLTab && activeTimelineTab === TimelineTabs.esql && ( = ({ sessionViewConfig, timelineDescription, }) => { - const isEsqlTabInTimelineDisabled = useIsExperimentalFeatureEnabled('timelineEsqlTabDisabled'); const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); - const isEsqlSettingEnabled = useKibana().services.configSettings.ESQLEnabled; const { hasAssistantPrivilege } = useAssistantAvailability(); const dispatch = useDispatch(); const getActiveTab = useMemo(() => getActiveTabSelector(), []); @@ -268,9 +271,14 @@ const TabsContentComponent: React.FC = ({ const getAppNotes = useMemo(() => getNotesSelector(), []); const getTimelineNoteIds = useMemo(() => getNoteIdsSelector(), []); const getTimelinePinnedEventNotes = useMemo(() => getEventIdToNoteIdsSelector(), []); + const { isESQLTabInTimelineEnabled } = useEsqlAvailability(); + const timelineESQLSavedSearch = useShallowEqualSelector((state) => + selectTimelineESQLSavedSearchId(state, timelineId) + ); const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId)); const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId)); + const shouldShowESQLTab = isESQLTabInTimelineEnabled || timelineESQLSavedSearch != null; const numberOfPinnedEvents = useShallowEqualSelector((state) => getNumberOfPinnedEvents(state, timelineId) @@ -373,7 +381,7 @@ const TabsContentComponent: React.FC = ({ {i18n.QUERY_TAB} {showTimeline && } - {!isEsqlTabInTimelineDisabled && isEsqlSettingEnabled && ( + {shouldShowESQLTab && ( time */ const selectTimelineKqlQuery = createSelector(selectTimelineById, (timeline) => timeline?.kqlQuery); +/** + * Selector that returns the timeline esql saved search id. + */ +export const selectTimelineESQLSavedSearchId = createSelector( + selectTimelineById, + (timeline) => timeline?.savedSearchId +); + /** * Selector that returns the kqlQuery.filterQuery.kuery.expression of a timeline. */ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 9281c317ab2e0..75b75b34139ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -430,7 +430,7 @@ export const previewRulesRoute = async ( ); break; case 'esql': - if (!config.settings.ESQLEnabled || config.experimentalFeatures.esqlRulesDisabled) { + if (config.experimentalFeatures.esqlRulesDisabled) { throw Error('ES|QL rule type is not supported'); } const esqlAlertType = previewRuleTypeWrapper(createEsqlAlertType(ruleOptions)); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 8cecb9c96345f..0564e6a95100c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -319,7 +319,7 @@ export class Plugin implements ISecuritySolutionPlugin { const securityRuleTypeWrapper = createSecurityRuleTypeWrapper(securityRuleTypeOptions); plugins.alerting.registerType(securityRuleTypeWrapper(createEqlAlertType(ruleOptions))); - if (config.settings.ESQLEnabled && !experimentalFeatures.esqlRulesDisabled) { + if (!experimentalFeatures.esqlRulesDisabled) { plugins.alerting.registerType(securityRuleTypeWrapper(createEsqlAlertType(ruleOptions))); } plugins.alerting.registerType( diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_serverless.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_serverless.cy.ts index f2b2b07975a04..95e95ca5e1439 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_serverless.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_serverless.cy.ts @@ -20,25 +20,19 @@ describe('Detection ES|QL rules, creation', { tags: ['@serverless'] }, () => { login(); }); - it('does not display ES|QL rule on form', function () { + it('should display ES|QL rule on form', function () { visit(CREATE_RULE_URL); // ensure, page is loaded and rule types are displayed cy.get(NEW_TERMS_TYPE).should('be.visible'); cy.get(THRESHOLD_TYPE).should('be.visible'); - // ES|QL rule tile should not be rendered - cy.get(ESQL_TYPE).should('not.exist'); + cy.get(ESQL_TYPE).should('exist'); }); - it('does not allow to create rule by API call', function () { + it('allow creation rule by API call', function () { createRule(getEsqlRule()).then((response) => { - expect(response.status).to.equal(400); - - expect(response.body).to.deep.equal({ - status_code: 400, - message: 'Rule type "siem.esqlRule" is not registered.', - }); + expect(response.status).to.equal(200); }); }); });