Skip to content

Commit

Permalink
[Security Solution] - Security solution ES|QL configurable via advanc…
Browse files Browse the repository at this point in the history
…ed setting (elastic#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>
(cherry picked from commit 963391e)
  • Loading branch information
michaelolo24 committed May 2, 2024
1 parent 2719518 commit 924d51f
Show file tree
Hide file tree
Showing 13 changed files with 168 additions and 56 deletions.
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,
});

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 = () => {
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 @@ -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();

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();

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

0 comments on commit 924d51f

Please sign in to comment.