Skip to content

Commit

Permalink
[8.14] [Security Solution] - Security solution ES|QL configurable via…
Browse files Browse the repository at this point in the history
… 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)](#181616)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Michael
Olorunnisola","email":"michael.olorunnisola@elastic.co"},"sourceCommit":{"committedDate":"2024-05-02T17:02:46Z","message":"[Security
Solution] - Security solution ES|QL configurable via advanced setting
(#181616)\n\n## Summary\r\n\r\nThis PR links the ESQL functionality in
security solution to the\r\n`discover:enableESQL` advanced setting. The
advanced setting will only\r\nbe present in ESS, but not
serverless\r\n\r\nThe way this should work to maintain parity with the
rest of Kibana such\r\nas discover and stack rules:\r\n\r\n- By default
ES|QL will be enabled across all Kibana\r\n- When the ES|QL advanced
setting is disabled:\r\n - Timeline\r\n - ES|QL tab should not be
accessible on any newly created timelines\r\n- Existing Timelines with
an ES|QL query should still have the tab\r\naccessible\r\n - Rules\r\n-
New ES|QL rule should not be available to be created in the
*Rule\r\nCreation* workflow\r\n - Existing ES|QL rules should still run
and be able to be edited\r\n\r\n\r\n\r\n**Timeline Demo
Video:**\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/17211684/d5429be9-de37-43e2-882d-687b3371beb4\r\n\r\n**Rules
Demo
Video:**\r\n\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/17211684/7df2fd11-bd2b-4e50-ad97-b6e1d0f7867a\r\n\r\n---------\r\n\r\nCo-authored-by:
Vitalii Dmyterko
<92328789+vitaliidm@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"963391ed0f7513c24c4827444feb971e460c53c0","branchLabelMapping":{"^v8.15.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Team:Threat
Hunting:Investigations","ci:build-serverless-image","v8.14.0","v8.15.0"],"number":181616,"url":"https://github.com/elastic/kibana/pull/181616","mergeCommit":{"message":"[Security
Solution] - Security solution ES|QL configurable via advanced setting
(#181616)\n\n## Summary\r\n\r\nThis PR links the ESQL functionality in
security solution to the\r\n`discover:enableESQL` advanced setting. The
advanced setting will only\r\nbe present in ESS, but not
serverless\r\n\r\nThe way this should work to maintain parity with the
rest of Kibana such\r\nas discover and stack rules:\r\n\r\n- By default
ES|QL will be enabled across all Kibana\r\n- When the ES|QL advanced
setting is disabled:\r\n - Timeline\r\n - ES|QL tab should not be
accessible on any newly created timelines\r\n- Existing Timelines with
an ES|QL query should still have the tab\r\naccessible\r\n - Rules\r\n-
New ES|QL rule should not be available to be created in the
*Rule\r\nCreation* workflow\r\n - Existing ES|QL rules should still run
and be able to be edited\r\n\r\n\r\n\r\n**Timeline Demo
Video:**\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/17211684/d5429be9-de37-43e2-882d-687b3371beb4\r\n\r\n**Rules
Demo
Video:**\r\n\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/17211684/7df2fd11-bd2b-4e50-ad97-b6e1d0f7867a\r\n\r\n---------\r\n\r\nCo-authored-by:
Vitalii Dmyterko
<92328789+vitaliidm@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"963391ed0f7513c24c4827444feb971e460c53c0"}},"sourceBranch":"main","suggestedTargetBranches":["8.14"],"targetPullRequestStates":[{"branch":"8.14","label":"v8.14.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.15.0","labelRegex":"^v8.15.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/181616","number":181616,"mergeCommit":{"message":"[Security
Solution] - Security solution ES|QL configurable via advanced setting
(#181616)\n\n## Summary\r\n\r\nThis PR links the ESQL functionality in
security solution to the\r\n`discover:enableESQL` advanced setting. The
advanced setting will only\r\nbe present in ESS, but not
serverless\r\n\r\nThe way this should work to maintain parity with the
rest of Kibana such\r\nas discover and stack rules:\r\n\r\n- By default
ES|QL will be enabled across all Kibana\r\n- When the ES|QL advanced
setting is disabled:\r\n - Timeline\r\n - ES|QL tab should not be
accessible on any newly created timelines\r\n- Existing Timelines with
an ES|QL query should still have the tab\r\naccessible\r\n - Rules\r\n-
New ES|QL rule should not be available to be created in the
*Rule\r\nCreation* workflow\r\n - Existing ES|QL rules should still run
and be able to be edited\r\n\r\n\r\n\r\n**Timeline Demo
Video:**\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/17211684/d5429be9-de37-43e2-882d-687b3371beb4\r\n\r\n**Rules
Demo
Video:**\r\n\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/17211684/7df2fd11-bd2b-4e50-ad97-b6e1d0f7867a\r\n\r\n---------\r\n\r\nCo-authored-by:
Vitalii Dmyterko
<92328789+vitaliidm@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"963391ed0f7513c24c4827444feb971e460c53c0"}}]}]
BACKPORT-->

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
michaelolo24 and kibanamachine authored May 2, 2024
1 parent 32fd7e4 commit d6fa87e
Show file tree
Hide file tree
Showing 13 changed files with 174 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,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]
);
};
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 d6fa87e

Please sign in to comment.