Skip to content

Commit

Permalink
[Security Serverless] - no net new capability to initiate/create inve…
Browse files Browse the repository at this point in the history
…stigate guides in timelines for 'essential tier' (#8700) (#181562)

## Summary

Addresses elastic/security-team#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="https://github.com/elastic/kibana/assets/2700761/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="https://github.com/elastic/kibana/assets/2700761/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>
  • Loading branch information
e40pud and kibanamachine authored Apr 26, 2024
1 parent bc87224 commit 13a968a
Show file tree
Hide file tree
Showing 16 changed files with 152 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
},
},
},
[ProductFeatureSecurityKey.investigationGuideInteractions]: {
privileges: {
all: {
ui: ['investigation-guide-interactions'],
},
read: {
ui: ['investigation-guide-interactions'],
},
},
},

[ProductFeatureSecurityKey.threatIntelligence]: {
privileges: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
EuiSelect,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
} from '@elastic/eui';
import numeral from '@elastic/numeral';
import { css } from '@emotion/react';
Expand All @@ -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';
Expand Down Expand Up @@ -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>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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} />;
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(
Expand All @@ -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>
Expand Down
Loading

0 comments on commit 13a968a

Please sign in to comment.