From 21ba8389b7d4eb2d4bb6debf0a0032c1c3608e5d Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Wed, 24 Apr 2024 14:30:59 +0200 Subject: [PATCH 1/3] [Security Serverless] - no net new capability to initiate/create investigate guides in timelines for 'essential tier' (#8700) --- .../features/src/product_features_keys.ts | 4 ++ .../src/security/product_feature_config.ts | 10 +++++ .../upselling/messages/index.tsx | 8 ++++ .../upselling/service/types.ts | 1 + .../components/markdown_editor/editor.tsx | 10 ++++- .../markdown_editor/plugins/index.ts | 12 +++++- .../plugins/insight/index.test.tsx | 24 +++--------- .../markdown_editor/plugins/insight/index.tsx | 24 +++++++----- .../plugins/osquery/plugin.tsx | 39 ++++++++++++------- .../plugins/osquery/renderer.tsx | 23 +++++++---- .../plugins/timeline/plugin.tsx | 35 ++++++++++------- .../plugins/timeline/processor.tsx | 11 +++++- .../common/pli/pli_config.ts | 1 + .../public/upselling/register_upsellings.tsx | 12 +++++- 14 files changed, 143 insertions(+), 71 deletions(-) diff --git a/x-pack/packages/security-solution/features/src/product_features_keys.ts b/x-pack/packages/security-solution/features/src/product_features_keys.ts index 9583c3ce9aec6..315dcb2730738 100644 --- a/x-pack/packages/security-solution/features/src/product_features_keys.ts +++ b/x-pack/packages/security-solution/features/src/product_features_keys.ts @@ -12,6 +12,10 @@ export enum ProductFeatureSecurityKey { * Enables Investigation guide in Timeline */ investigationGuide = 'investigation_guide', + /** + * Enables Investigation guide interactions (e.g., osquery, timelines, etc.) + */ + investigationGuideInteractions = 'investigation_guide_interactions', /** * Enables access to the Endpoint List and associated views that allows management of hosts * running endpoint security diff --git a/x-pack/packages/security-solution/features/src/security/product_feature_config.ts b/x-pack/packages/security-solution/features/src/security/product_feature_config.ts index 330cf5a8a0a46..d9f52ebf54192 100644 --- a/x-pack/packages/security-solution/features/src/security/product_feature_config.ts +++ b/x-pack/packages/security-solution/features/src/security/product_feature_config.ts @@ -42,6 +42,16 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature }, }, }, + [ProductFeatureSecurityKey.investigationGuideInteractions]: { + privileges: { + all: { + ui: ['investigation-guide-interactions'], + }, + read: { + ui: ['investigation-guide-interactions'], + }, + }, + }, [ProductFeatureSecurityKey.threatIntelligence]: { privileges: { diff --git a/x-pack/packages/security-solution/upselling/messages/index.tsx b/x-pack/packages/security-solution/upselling/messages/index.tsx index b834c7e6ad4de..722a711995d01 100644 --- a/x-pack/packages/security-solution/upselling/messages/index.tsx +++ b/x-pack/packages/security-solution/upselling/messages/index.tsx @@ -15,6 +15,14 @@ export const UPGRADE_INVESTIGATION_GUIDE = (requiredLicense: string) => }, }); +export const UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS = (requiredLicense: string) => + i18n.translate('securitySolutionPackages.markdown.investigationGuideInteractions.upsell', { + defaultMessage: 'Upgrade to {requiredLicense} to make use of investigation guide interactions', + values: { + requiredLicense, + }, + }); + export const UPGRADE_ALERT_ASSIGNMENTS = (requiredLicense: string) => i18n.translate('securitySolutionPackages.alertAssignments.upsell', { defaultMessage: 'Upgrade to {requiredLicense} to make use of alert assignments', diff --git a/x-pack/packages/security-solution/upselling/service/types.ts b/x-pack/packages/security-solution/upselling/service/types.ts index 31aace488ab97..0b0d5b6e930f5 100644 --- a/x-pack/packages/security-solution/upselling/service/types.ts +++ b/x-pack/packages/security-solution/upselling/service/types.ts @@ -21,6 +21,7 @@ export type UpsellingSectionId = export type UpsellingMessageId = | 'investigation_guide' + | 'investigation_guide_interactions' | 'alert_assignments' | 'alert_suppression_rule_form' | 'alert_suppression_rule_details'; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx index 2f439c55a7d1c..5662c68ec1c32 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx @@ -74,9 +74,15 @@ const MarkdownEditorComponent = forwardRef { - return includePlugins ? uiPlugins({ insightsUpsellingMessage }) : undefined; - }, [insightsUpsellingMessage, includePlugins]); + return includePlugins + ? uiPlugins({ + insightsUpsellingMessage, + interactionsUpsellingMessage, + }) + : undefined; + }, [includePlugins, insightsUpsellingMessage, interactionsUpsellingMessage]); // @ts-expect-error update types useImperativeHandle(ref, () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index a34ac40ec8a28..607ed6d94959b 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -22,16 +22,24 @@ export const platinumOnlyPluginTokens = [insightMarkdownPlugin.insightPrefix]; export const uiPlugins = ({ insightsUpsellingMessage, + interactionsUpsellingMessage, }: { insightsUpsellingMessage: string | null; + interactionsUpsellingMessage: string | null; }) => { const currentPlugins = nonStatefulUiPlugins.map((plugin) => plugin.name); const insightPluginWithLicense = insightMarkdownPlugin.plugin({ insightsUpsellingMessage, }); + const timelinePluginWithLicense = timelineMarkdownPlugin.plugin({ + interactionsUpsellingMessage, + }); + const osqueryPluginWithLicense = osqueryMarkdownPlugin.plugin({ + interactionsUpsellingMessage, + }); if (currentPlugins.includes(insightPluginWithLicense.name) === false) { - nonStatefulUiPlugins.push(timelineMarkdownPlugin.plugin); - nonStatefulUiPlugins.push(osqueryMarkdownPlugin.plugin); + nonStatefulUiPlugins.push(timelinePluginWithLicense); + nonStatefulUiPlugins.push(osqueryPluginWithLicense); nonStatefulUiPlugins.push(insightPluginWithLicense); } else { // When called for the second time we need to update insightMarkdownPlugin diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx index 2b4ae4d2d9fcf..8ea10b1c54f8c 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx @@ -14,10 +14,10 @@ import { DEFAULT_TO, } from '../../../../../../common/constants'; import { KibanaServices } from '../../../../lib/kibana'; -import { licenseService } from '../../../../hooks/use_license'; import type { DefaultTimeRangeSetting } from '../../../../utils/default_date_settings'; import { plugin, renderer as Renderer } from '.'; import type { InvestigateInTimelineButtonProps } from '../../../event_details/table/investigate_in_timeline_button'; +import { useUpsellingMessage } from '../../../../hooks/use_upselling'; jest.mock('../../../../lib/kibana'); const mockGetServices = KibanaServices.get as jest.Mock; @@ -59,24 +59,12 @@ const mockTimeRange = ( })); }; -jest.mock('../../../../hooks/use_license', () => { - const licenseServiceInstance = { - isPlatinumPlus: jest.fn(), - isEnterprise: jest.fn(() => true), - }; - return { - licenseService: licenseServiceInstance, - useLicense: () => { - return licenseServiceInstance; - }, - }; -}); -const licenseServiceMock = licenseService as jest.Mocked; +jest.mock('../../../../hooks/use_upselling'); describe('insight component renderer', () => { - describe('when license is at least platinum plus', () => { + describe('when there is no upselling message', () => { beforeAll(() => { - licenseServiceMock.isPlatinumPlus.mockReturnValue(true); + (useUpsellingMessage as jest.Mock).mockReturnValue(null); mockTimeRange(null); }); it('renders correctly with valid date strings with no timestamp from results', () => { @@ -106,9 +94,9 @@ describe('insight component renderer', () => { }); }); - describe('when license is not at least platinum plus', () => { + describe('when there is an upselling message', () => { beforeAll(() => { - licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + (useUpsellingMessage as jest.Mock).mockReturnValue('Go for Platinum!'); mockTimeRange(null); }); it('renders a disabled eui button with label', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 2faca21677863..d5a9775bdcc6a 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -28,6 +28,7 @@ import { EuiSelect, EuiFlexGroup, EuiFlexItem, + EuiToolTip, } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { css } from '@emotion/react'; @@ -36,6 +37,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { Filter } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import { useForm, FormProvider, useController } from 'react-hook-form'; +import { useUpsellingMessage } from '../../../../hooks/use_upselling'; import { useAppToasts } from '../../../../hooks/use_app_toasts'; import { useKibana } from '../../../../lib/kibana'; import { useInsightQuery } from './use_insight_query'; @@ -240,19 +242,21 @@ const InsightComponent = ({ relativeFrom, relativeTo, }: InsightComponentProps) => { - const isPlatinum = useLicense().isPlatinumPlus(); + const insightsUpsellingMessage = useUpsellingMessage('investigation_guide'); - if (isPlatinum === false) { + if (insightsUpsellingMessage) { return ( <> - - {`${label}`} - + + + {`${label}`} + +
{description}
); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx index 7e12c1c42d001..78a8c2e7b5d50 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx @@ -156,19 +156,28 @@ const OsqueryEditorComponent = ({ const OsqueryEditor = React.memo(OsqueryEditorComponent); -export const plugin = { - name: 'osquery', - button: { - label: 'Osquery', - iconType: 'logoOsquery', - }, - helpText: ( -
- - {'!{osquery{options}}'} - - -
- ), - editor: OsqueryEditor, +export const plugin = ({ + interactionsUpsellingMessage, +}: { + interactionsUpsellingMessage: string | null; +}) => { + return { + name: 'osquery', + button: { + label: interactionsUpsellingMessage ?? 'Osquery', + 'aria-label': 'aria-label-test1', + iconType: 'logoOsquery', + isDisabled: !!interactionsUpsellingMessage, + 'data-test-subj': 'test-test-test', + }, + helpText: ( +
+ + {'!{osquery{options}}'} + + +
+ ), + editor: OsqueryEditor, + }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx index 20bdb4d7a9e56..04963e70f9cfa 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx @@ -10,8 +10,9 @@ import React, { useCallback, useContext, useMemo, useState } from 'react'; import { reduce } from 'lodash'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiToolTip } from '@elastic/eui'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { useUpsellingMessage } from '../../../../hooks/use_upselling'; import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; import { expandDottedObject } from '../../../../../../common/utils/expand_dotted'; import OsqueryLogo from './osquery_icon/osquery.svg'; @@ -40,6 +41,8 @@ export const OsqueryRenderer = ({ const handleClose = useCallback(() => setShowFlyout(false), [setShowFlyout]); + const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions'); + const ecsData = useMemo(() => { const fieldsMap: Record = reduce( data, @@ -54,12 +57,18 @@ export const OsqueryRenderer = ({ return ( <> - - {configuration.label ?? - i18n.translate('xpack.securitySolution.markdown.osquery.runOsqueryButtonLabel', { - defaultMessage: 'Run Osquery', - })} - + + + {configuration.label ?? + i18n.translate('xpack.securitySolution.markdown.osquery.runOsqueryButtonLabel', { + defaultMessage: 'Run Osquery', + })} + + {showFlyout && ( = ({ onClosePopover const TimelineEditor = memo(TimelineEditorComponent); -export const plugin: EuiMarkdownEditorUiPlugin = { - name: ID, - button: { - label: i18n.INSERT_TIMELINE, - iconType: 'timeline', - }, - helpText: ( - - {'[title](url)'} - - ), - editor: function editor({ node, onSave, onCancel }) { - return ; - }, +export const plugin = ({ + interactionsUpsellingMessage, +}: { + interactionsUpsellingMessage: string | null; +}): EuiMarkdownEditorUiPlugin => { + return { + name: ID, + button: { + label: interactionsUpsellingMessage ?? i18n.INSERT_TIMELINE, + iconType: 'timeline', + isDisabled: !!interactionsUpsellingMessage, + }, + helpText: ( + + {'[title](url)'} + + ), + editor: function editor({ node, onSave, onCancel }) { + return ; + }, + }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx index 307378b2076bf..c2460cf7f5d19 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx @@ -8,6 +8,7 @@ import React, { useCallback, memo } from 'react'; import { EuiToolTip, EuiLink } from '@elastic/eui'; +import { useUpsellingMessage } from '../../../../hooks/use_upselling'; import { useTimelineClick } from '../../../../utils/timeline/use_timeline_click'; import type { TimelineProps } from './types'; import * as i18n from './translations'; @@ -20,6 +21,8 @@ export const TimelineMarkDownRendererComponent: React.FC = ({ }) => { const { addError } = useAppToasts(); + const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions'); + const handleTimelineClick = useTimelineClick(); const onError = useCallback( @@ -37,8 +40,12 @@ export const TimelineMarkDownRendererComponent: React.FC = ({ [id, graphEventId, handleTimelineClick, onError] ); return ( - - + + {title} diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts index f3b3407e4fb69..4798b4923de96 100644 --- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -23,6 +23,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = { ProductFeatureKey.advancedInsights, ProductFeatureKey.assistant, ProductFeatureKey.investigationGuide, + ProductFeatureKey.investigationGuideInteractions, ProductFeatureKey.threatIntelligence, ProductFeatureKey.casesConnectors, ProductFeatureKey.externalRuleActions, diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx index 1edd2857017f7..a16bb4272f720 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx @@ -14,7 +14,10 @@ import type { } from '@kbn/security-solution-upselling/service/types'; import type { UpsellingService } from '@kbn/security-solution-upselling/service'; import React from 'react'; -import { UPGRADE_INVESTIGATION_GUIDE } from '@kbn/security-solution-upselling/messages'; +import { + UPGRADE_INVESTIGATION_GUIDE, + UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS, +} from '@kbn/security-solution-upselling/messages'; import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; import type { ProductFeatureKeyType } from '@kbn/security-solution-features'; import { @@ -163,4 +166,11 @@ export const upsellingMessages: UpsellingMessages = [ getProductTypeByPLI(ProductFeatureKey.investigationGuide) ?? '' ), }, + { + id: 'investigation_guide_interactions', + pli: ProductFeatureKey.investigationGuideInteractions, + message: UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS( + getProductTypeByPLI(ProductFeatureKey.investigationGuideInteractions) ?? '' + ), + }, ]; From 7dda6fd35da3ee0ca4c5e51efd3cb71cfd5abc29 Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Thu, 25 Apr 2024 17:24:36 +0200 Subject: [PATCH 2/3] Fix tests --- .../security_solution/public/cases/pages/index.tsx | 5 ++++- .../common/components/markdown_editor/renderer.test.tsx | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index a4d43043b49e9..29f149f3988a9 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -39,6 +39,7 @@ import { useInsertTimeline } from '../components/use_insert_timeline'; import * as timelineMarkdownPlugin from '../../common/components/markdown_editor/plugins/timeline'; import { DetailsPanel } from '../../timelines/components/side_panel'; import { useFetchAlertData } from './use_fetch_alert_data'; +import { useUpsellingMessage } from '../../common/hooks/use_upselling'; const TimelineDetailsPanel = () => { const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.detections); @@ -69,6 +70,8 @@ const CaseContainerComponent: React.FC = () => { [detectionsFormatUrl, detectionsUrlSearch] ); + const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions'); + const showAlertDetails = useCallback( (alertId: string, index: string) => { if (isSecurityFlyoutEnabled) { @@ -187,7 +190,7 @@ const CaseContainerComponent: React.FC = () => { editor_plugins: { parsingPlugin: timelineMarkdownPlugin.parser, processingPluginRenderer: timelineMarkdownPlugin.renderer, - uiPlugin: timelineMarkdownPlugin.plugin, + uiPlugin: timelineMarkdownPlugin.plugin({ interactionsUpsellingMessage }), }, hooks: { useInsertTimeline, diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx index b6d6f1bf0e69e..cc8f857e55b49 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx @@ -11,6 +11,8 @@ import { render } from '@testing-library/react'; import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { TestProviders } from '../../mock'; import { MarkdownRenderer } from './renderer'; +import { UpsellingService } from '@kbn/security-solution-upselling/service'; +import { UpsellingProvider } from '../upselling_provider'; jest.mock('../../utils/default_date_settings', () => { const original = jest.requireActual('../../utils/default_date_settings'); @@ -59,6 +61,8 @@ jest.mock('../../hooks/use_app_toasts', () => ({ }), })); +const mockUpselling = new UpsellingService(); + describe('Markdown', () => { describe('markdown links', () => { const markdownWithLink = 'A link to an external site [External Site](https://google.com)'; @@ -114,7 +118,9 @@ describe('Markdown', () => { test('displays an upgrade message with a premium markdown plugin', () => { const { queryByText, getByText } = render( - {`!{investigate{"label": "", "providers": [[{"field": "event.id", "value": "{{kibana.alert.original_event.id}}", "queryType": "phrase", "excluded": "false"}]]}}`} + + {`!{investigate{"label": "", "providers": [[{"field": "event.id", "value": "{{kibana.alert.original_event.id}}", "queryType": "phrase", "excluded": "false"}]]}}`} + ); From 3b0939fde8e261a8c5382937878a1bf362251812 Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Fri, 26 Apr 2024 18:36:01 +0200 Subject: [PATCH 3/3] Remove debugging code --- .../components/markdown_editor/plugins/osquery/plugin.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx index 78a8c2e7b5d50..6cc54184db60c 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx @@ -165,10 +165,8 @@ export const plugin = ({ name: 'osquery', button: { label: interactionsUpsellingMessage ?? 'Osquery', - 'aria-label': 'aria-label-test1', iconType: 'logoOsquery', isDisabled: !!interactionsUpsellingMessage, - 'data-test-subj': 'test-test-test', }, helpText: (