From 97b075961ecc327ff415de9d5b4755c88b415410 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 2 May 2024 13:02:46 -0400 Subject: [PATCH] [Security Solution] - Security solution ES|QL configurable via advanced setting (#181616) ## Summary This PR links the ESQL functionality in security solution to the `discover:enableESQL` advanced setting. The advanced setting will only be present in ESS, but not serverless The way this should work to maintain parity with the rest of Kibana such as discover and stack rules: - By default ES|QL will be enabled across all Kibana - When the ES|QL advanced setting is disabled: - Timeline - ES|QL tab should not be accessible on any newly created timelines - Existing Timelines with an ES|QL query should still have the tab accessible - Rules - New ES|QL rule should not be available to be created in the *Rule Creation* workflow - Existing ES|QL rules should still run and be able to be edited **Timeline Demo Video:** https://github.com/elastic/kibana/assets/17211684/d5429be9-de37-43e2-882d-687b3371beb4 **Rules Demo Video:** https://github.com/elastic/kibana/assets/17211684/7df2fd11-bd2b-4e50-ad97-b6e1d0f7867a --------- Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Co-authored-by: kibanamachine <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 | 28 +++++++ .../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, 168 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 5ebb22486dc4317..88770178a34934f 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 5d5c40fa2b48c97..6dd9967f6e3fdf6 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 ba849f2c85864e0..000000000000000 --- 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 239c49088e644fe..000000000000000 --- 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 000000000000000..a9df259addf49cd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/esql/use_esql_availability.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useKibana } from '../../lib/kibana'; +import { useIsExperimentalFeatureEnabled } from '../use_experimental_features'; + +export const useEsqlAvailability = () => { + const { uiSettings } = useKibana().services; + const isEsqlAdvancedSettingEnabled = uiSettings?.get('discover:enableESQL'); + 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 73bdf48623e2a88..9930dc7f626ad67 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 c93b107849c2b03..2b222fd3c393ff3 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 8c9d2dc1298e534..2e164677735dd46 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 5cbaa4483430260..8c4137bf3d38665 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 @@ -431,7 +431,7 @@ export const previewRulesRoute = ( ); 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 b0eb25bd3c18f2f..e97abbb1f047471 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -320,7 +320,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 f2b2b07975a043b..95e95ca5e143934 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); }); }); });