[Security Serverless] - no net new capability to initiate/create investigate guides in timelines for 'essential tier' (#8700) (#181562)

## Summary

Addresses https://github.com/elastic/security-team/issues/8700

With these changes we disable Interactive Investigation guides
interactions buttons (timelines + OSquery interactive actions) for
'Essential tier' in Serverless.

For Investigation guides in the Detection rules:

**Create rule page -> Advanced settings - > Investigation guide**

osquery and timeline buttons should be inactive and have an upgrade
callout

<img width="1033" alt="Screenshot 2024-04-24 at 15 08 32"
src="91f5b5e0-9efe-4ee3-b0c8-e3e75c4782b7">

**Rule details page -> Investigations guide**

buttons in the markdown should be inactive and have an upgrade callout

<img width="736" alt="Screenshot 2024-04-24 at 15 09 07"
src="a1eca15a-ea0a-4346-b193-d452a38849f1">

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ]
[Tests](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5754)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ievgen Sorokopud 2024-04-26 20:54:05 +02:00 committed by GitHub
parent bc87224b41
commit 13a968a447
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 152 additions and 73 deletions

View file

@ -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

View file

@ -42,6 +42,16 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
},
},
},
[ProductFeatureSecurityKey.investigationGuideInteractions]: {
privileges: {
all: {
ui: ['investigation-guide-interactions'],
},
read: {
ui: ['investigation-guide-interactions'],
},
},
},
[ProductFeatureSecurityKey.threatIntelligence]: {
privileges: {

View file

@ -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',

View file

@ -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';

View file

@ -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,

View file

@ -74,9 +74,15 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
}, [autoFocusDisabled]);
const insightsUpsellingMessage = useUpsellingMessage('investigation_guide');
const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions');
const uiPluginsWithState = useMemo(() => {
return includePlugins ? uiPlugins({ insightsUpsellingMessage }) : undefined;
}, [insightsUpsellingMessage, includePlugins]);
return includePlugins
? uiPlugins({
insightsUpsellingMessage,
interactionsUpsellingMessage,
})
: undefined;
}, [includePlugins, insightsUpsellingMessage, interactionsUpsellingMessage]);
// @ts-expect-error update types
useImperativeHandle(ref, () => {

View file

@ -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

View file

@ -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<typeof licenseService>;
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', () => {

View file

@ -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 (
<>
<EuiButton
isDisabled={true}
iconSide={'left'}
iconType={'timeline'}
data-test-subj="insight-investigate-in-timeline-button"
>
{`${label}`}
</EuiButton>
<EuiToolTip content={insightsUpsellingMessage}>
<EuiButton
isDisabled={true}
iconSide={'left'}
iconType={'timeline'}
data-test-subj="insight-investigate-in-timeline-button"
>
{`${label}`}
</EuiButton>
</EuiToolTip>
<div>{description}</div>
</>
);

View file

@ -156,19 +156,26 @@ const OsqueryEditorComponent = ({
const OsqueryEditor = React.memo(OsqueryEditorComponent);
export const plugin = {
name: 'osquery',
button: {
label: 'Osquery',
iconType: 'logoOsquery',
},
helpText: (
<div>
<EuiCodeBlock language="md" fontSize="l" paddingSize="s" isCopyable>
{'!{osquery{options}}'}
</EuiCodeBlock>
<EuiSpacer size="s" />
</div>
),
editor: OsqueryEditor,
export const plugin = ({
interactionsUpsellingMessage,
}: {
interactionsUpsellingMessage: string | null;
}) => {
return {
name: 'osquery',
button: {
label: interactionsUpsellingMessage ?? 'Osquery',
iconType: 'logoOsquery',
isDisabled: !!interactionsUpsellingMessage,
},
helpText: (
<div>
<EuiCodeBlock language="md" fontSize="l" paddingSize="s" isCopyable>
{'!{osquery{options}}'}
</EuiCodeBlock>
<EuiSpacer size="s" />
</div>
),
editor: OsqueryEditor,
};
};

View file

@ -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<string, string> = reduce(
data,
@ -54,12 +57,18 @@ export const OsqueryRenderer = ({
return (
<>
<StyledEuiButton iconType={OsqueryLogo} onClick={handleOpen}>
{configuration.label ??
i18n.translate('xpack.securitySolution.markdown.osquery.runOsqueryButtonLabel', {
defaultMessage: 'Run Osquery',
})}
</StyledEuiButton>
<EuiToolTip content={interactionsUpsellingMessage}>
<StyledEuiButton
iconType={OsqueryLogo}
onClick={handleOpen}
disabled={!!interactionsUpsellingMessage}
>
{configuration.label ??
i18n.translate('xpack.securitySolution.markdown.osquery.runOsqueryButtonLabel', {
defaultMessage: 'Run Osquery',
})}
</StyledEuiButton>
</EuiToolTip>
{showFlyout && (
<OsqueryFlyout
defaultValues={{

View file

@ -75,18 +75,25 @@ const TimelineEditorComponent: React.FC<TimelineEditorProps> = ({ onClosePopover
const TimelineEditor = memo(TimelineEditorComponent);
export const plugin: EuiMarkdownEditorUiPlugin = {
name: ID,
button: {
label: i18n.INSERT_TIMELINE,
iconType: 'timeline',
},
helpText: (
<EuiCodeBlock language="md" paddingSize="s" fontSize="l">
{'[title](url)'}
</EuiCodeBlock>
),
editor: function editor({ node, onSave, onCancel }) {
return <TimelineEditor onClosePopover={onCancel} onInsert={onSave} />;
},
export const plugin = ({
interactionsUpsellingMessage,
}: {
interactionsUpsellingMessage: string | null;
}): EuiMarkdownEditorUiPlugin => {
return {
name: ID,
button: {
label: interactionsUpsellingMessage ?? i18n.INSERT_TIMELINE,
iconType: 'timeline',
isDisabled: !!interactionsUpsellingMessage,
},
helpText: (
<EuiCodeBlock language="md" paddingSize="s" fontSize="l">
{'[title](url)'}
</EuiCodeBlock>
),
editor: function editor({ node, onSave, onCancel }) {
return <TimelineEditor onClosePopover={onCancel} onInsert={onSave} />;
},
};
};

View file

@ -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<TimelineProps> = ({
}) => {
const { addError } = useAppToasts();
const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions');
const handleTimelineClick = useTimelineClick();
const onError = useCallback(
@ -37,8 +40,12 @@ export const TimelineMarkDownRendererComponent: React.FC<TimelineProps> = ({
[id, graphEventId, handleTimelineClick, onError]
);
return (
<EuiToolTip content={i18n.TIMELINE_ID(id ?? '')}>
<EuiLink onClick={onClickTimeline} data-test-subj={`markdown-timeline-link-${id}`}>
<EuiToolTip content={interactionsUpsellingMessage ?? i18n.TIMELINE_ID(id ?? '')}>
<EuiLink
onClick={onClickTimeline}
disabled={!!interactionsUpsellingMessage}
data-test-subj={`markdown-timeline-link-${id}`}
>
{title}
</EuiLink>
</EuiToolTip>

View file

@ -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(
<TestProviders>
<MarkdownRenderer>{`!{investigate{"label": "", "providers": [[{"field": "event.id", "value": "{{kibana.alert.original_event.id}}", "queryType": "phrase", "excluded": "false"}]]}}`}</MarkdownRenderer>
<UpsellingProvider upsellingService={mockUpselling}>
<MarkdownRenderer>{`!{investigate{"label": "", "providers": [[{"field": "event.id", "value": "{{kibana.alert.original_event.id}}", "queryType": "phrase", "excluded": "false"}]]}}`}</MarkdownRenderer>
</UpsellingProvider>
</TestProviders>
);

View file

@ -23,6 +23,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
ProductFeatureKey.advancedInsights,
ProductFeatureKey.assistant,
ProductFeatureKey.investigationGuide,
ProductFeatureKey.investigationGuideInteractions,
ProductFeatureKey.threatIntelligence,
ProductFeatureKey.casesConnectors,
ProductFeatureKey.externalRuleActions,

View file

@ -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) ?? ''
),
},
];