Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] - Security solution ES|QL configurable via advanced setting #181616

Merged
1 change: 0 additions & 1 deletion config/serverless.security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 0 additions & 5 deletions x-pack/plugins/security_solution/common/config_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ export interface ConfigSettings {
* Index Lifecycle Management (ILM) feature enabled.
*/
ILMEnabled: boolean;
/**
* ESQL queries enabled.
*/
ESQLEnabled: boolean;
}

/**
Expand All @@ -22,7 +18,6 @@ export interface ConfigSettings {
*/
export const defaultSettings: ConfigSettings = Object.freeze({
ILMEnabled: true,
ESQLEnabled: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to remove it from the serverless config

ESQLEnabled: false, # ES|QL disabled, not supported by serverless Elasticsearch

});

type ConfigSettingsKey = keyof ConfigSettings;
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even though this hook is pretty straightforward to understand, if you want I would add a short documentation to describe what the hook does. Up to you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, will add a comment in the PR right after this! Hoping to make the next BC!

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]
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('SelectRuleType', () => {
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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();
});

Let's also add unit test for the case covered in https://github.com/elastic/kibana/pull/181616/files#r1585195094

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks!

it('should not render "esql" rule type if esql rule is not enabled', () => {
useIsEsqlRuleTypeEnabledMock.mockReturnValueOnce(false);
useEsqlAvailabilityMock.mockReturnValueOnce(false);
const Component = () => {
const field = useFormFieldMock();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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[];
Expand All @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule type icon should be displayed when editing rule

Screenshot 2024-04-30 at 17 37 29

{isEsqlRuleTypeEnabled && (!isUpdateView || esqlSelectableConfig.isSelected) && (

I think this change should do it

          {((!isUpdateView && isEsqlRuleTypeEnabled) || esqlSelectableConfig.isSelected) && (

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, thanks @vitaliidm ! Completed here: 9b73610


const eqlSelectableConfig = useMemo(
() => ({
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 }) => ({
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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}`}
Expand Down Expand Up @@ -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(), []);
Expand All @@ -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)
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,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));
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});