Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
Comment thread
semd marked this conversation as resolved.

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