mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:**d5429be9
-de37-43e2-882d-687b3371beb4 **Rules Demo Video:**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>
This commit is contained in:
parent
187f22a2b0
commit
963391ed0f
13 changed files with 168 additions and 56 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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]
|
||||
);
|
||||
};
|
|
@ -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<unknown>({ value: 'esql' });
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<SelectRuleType
|
||||
describedByIds={[]}
|
||||
field={field}
|
||||
isUpdateView={true}
|
||||
hasValidLicense={true}
|
||||
isMlAdmin={true}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
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();
|
||||
|
||||
|
|
|
@ -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<SelectRuleTypeProps> = 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<SelectRuleTypeProps> = memo(
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{isEsqlRuleTypeEnabled && (!isUpdateView || esqlSelectableConfig.isSelected) && (
|
||||
{((!isUpdateView && isEsqlRuleTypeEnabled) || esqlSelectableConfig.isSelected) && (
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
data-test-subj="esqlRuleType"
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { createMockStore, mockGlobalState } from '../../../../common/mock';
|
||||
import { TestProviders } from '../../../../common/mock/test_providers';
|
||||
|
||||
import { TabsContent } from '.';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { TimelineType } from '../../../../../common/api/timeline';
|
||||
import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
jest.mock('../../../../common/hooks/esql/use_esql_availability', () => ({
|
||||
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(
|
||||
<TestProviders>
|
||||
<TabsContent {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(screen.getByTestId(esqlTabSubj)).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not show the esql tab when the advanced setting is disabled', async () => {
|
||||
useEsqlAvailabilityMock.mockReturnValue({
|
||||
isESQLTabInTimelineEnabled: false,
|
||||
});
|
||||
render(
|
||||
<TestProviders>
|
||||
<TabsContent {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestProviders store={mockStore}>
|
||||
<TabsContent {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId(esqlTabSubj)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<ActiveTimelineTabProps>(
|
|||
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<ActiveTimelineTabProps>(
|
|||
timelineId={timelineId}
|
||||
/>
|
||||
</HideShowContainer>
|
||||
{showTimeline && isEsqlSettingEnabled && activeTimelineTab === TimelineTabs.esql && (
|
||||
{showTimeline && shouldShowESQLTab && activeTimelineTab === TimelineTabs.esql && (
|
||||
<HideShowContainer
|
||||
$isVisible={true}
|
||||
data-test-subj={`timeline-tab-content-${TimelineTabs.esql}`}
|
||||
|
@ -257,9 +262,7 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
|
|||
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<BasicTimelineTab> = ({
|
|||
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<BasicTimelineTab> = ({
|
|||
<span>{i18n.QUERY_TAB}</span>
|
||||
{showTimeline && <TimelineEventsCountBadge />}
|
||||
</StyledEuiTab>
|
||||
{!isEsqlTabInTimelineDisabled && isEsqlSettingEnabled && (
|
||||
{shouldShowESQLTab && (
|
||||
<StyledEuiTab
|
||||
data-test-subj={`timelineTabs-${TimelineTabs.esql}`}
|
||||
onClick={setEsqlAsActiveTab}
|
||||
|
|
|
@ -116,6 +116,14 @@ const selectTimelineType = createSelector(selectTimelineById, (timeline) => 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.
|
||||
*/
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue