mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[SecuritySolution] Move related alerts by process ancestry behind platinum subscription (#138419) (#138605)
* feat: show an upsell for users without platinum license
* test: add license-related tests
* feat: re-add the feature flag
* nit: use EuiLink
* fix: check for length as well
* chore: move data check to helper
This reduced the overall complexity of the module, which made the eslint complexity exception obsolete
* tests:
* tests: fix tests
* tests: fix integration tests
(cherry picked from commit 785532eab6
)
Co-authored-by: Jan Monschke <jan.monschke@elastic.co>
This commit is contained in:
parent
74804fc775
commit
dba180eb6f
11 changed files with 227 additions and 25 deletions
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* 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 type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
|
||||||
|
|
||||||
|
type TimelineEventsDetailsItemWithValues = TimelineEventsDetailsItem & {
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the `item` has a non-empty `values` array
|
||||||
|
*/
|
||||||
|
export function hasData(
|
||||||
|
item?: TimelineEventsDetailsItem
|
||||||
|
): item is TimelineEventsDetailsItemWithValues {
|
||||||
|
return Boolean(item && item.values && item.values.length);
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ interface Props {
|
||||||
state: InsightAccordionState;
|
state: InsightAccordionState;
|
||||||
text: string;
|
text: string;
|
||||||
renderContent: () => ReactNode;
|
renderContent: () => ReactNode;
|
||||||
|
extraAction?: EuiAccordionProps['extraAction'];
|
||||||
onToggle?: EuiAccordionProps['onToggle'];
|
onToggle?: EuiAccordionProps['onToggle'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ interface Props {
|
||||||
* It wraps logic and custom styling around the loading, error and success states of an insight section.
|
* It wraps logic and custom styling around the loading, error and success states of an insight section.
|
||||||
*/
|
*/
|
||||||
export const InsightAccordion = React.memo<Props>(
|
export const InsightAccordion = React.memo<Props>(
|
||||||
({ prefix, state, text, renderContent, onToggle = noop }) => {
|
({ prefix, state, text, renderContent, onToggle = noop, extraAction }) => {
|
||||||
const accordionId = useGeneratedHtmlId({ prefix });
|
const accordionId = useGeneratedHtmlId({ prefix });
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
@ -54,6 +55,7 @@ export const InsightAccordion = React.memo<Props>(
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
|
extraAction={extraAction}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'success':
|
case 'success':
|
||||||
|
@ -64,6 +66,7 @@ export const InsightAccordion = React.memo<Props>(
|
||||||
buttonContent={text}
|
buttonContent={text}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
paddingSize="l"
|
paddingSize="l"
|
||||||
|
extraAction={extraAction}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</StyledAccordion>
|
</StyledAccordion>
|
||||||
|
|
|
@ -10,8 +10,11 @@ import React from 'react';
|
||||||
|
|
||||||
import { TestProviders } from '../../../mock';
|
import { TestProviders } from '../../../mock';
|
||||||
|
|
||||||
|
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
|
||||||
import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__';
|
import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__';
|
||||||
import { useGetUserCasesPermissions } from '../../../lib/kibana';
|
import { useGetUserCasesPermissions } from '../../../lib/kibana';
|
||||||
|
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
||||||
|
import { licenseService } from '../../../hooks/use_license';
|
||||||
import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils';
|
import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils';
|
||||||
import { Insights } from './insights';
|
import { Insights } from './insights';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
|
@ -39,6 +42,46 @@ jest.mock('../../../lib/kibana', () => {
|
||||||
});
|
});
|
||||||
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
|
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
|
||||||
|
|
||||||
|
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_experimental_features', () => ({
|
||||||
|
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||||
|
}));
|
||||||
|
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||||
|
|
||||||
|
const data: TimelineEventsDetailsItem[] = [
|
||||||
|
{
|
||||||
|
category: 'process',
|
||||||
|
field: 'process.entity_id',
|
||||||
|
isObjectArray: false,
|
||||||
|
values: ['32082y34028u34'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'kibana',
|
||||||
|
field: 'kibana.alert.ancestors.id',
|
||||||
|
isObjectArray: false,
|
||||||
|
values: ['woeurhw98rhwr'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'kibana',
|
||||||
|
field: 'kibana.alert.rule.parameters.index',
|
||||||
|
isObjectArray: false,
|
||||||
|
values: ['fakeindex'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('Insights', () => {
|
describe('Insights', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockUseGetUserCasesPermissions.mockReturnValue(noCasesPermissions());
|
mockUseGetUserCasesPermissions.mockReturnValue(noCasesPermissions());
|
||||||
|
@ -77,4 +120,57 @@ describe('Insights', () => {
|
||||||
})
|
})
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with feature flag enabled', () => {
|
||||||
|
describe('with platinum license', () => {
|
||||||
|
it('should show insights for related alerts by process ancestry', () => {
|
||||||
|
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestProviders>
|
||||||
|
<Insights browserFields={{}} eventId="test" data={data} timelineId="" />
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('related-alerts-by-ancestry')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('link', { name: new RegExp(i18n.ALERT_UPSELL) })
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('without platinum license', () => {
|
||||||
|
it('should show an upsell for related alerts by process ancestry', () => {
|
||||||
|
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestProviders>
|
||||||
|
<Insights browserFields={{}} eventId="test" data={data} timelineId="" />
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('link', { name: new RegExp(i18n.ALERT_UPSELL) })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('related-alerts-by-ancestry')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with feature flag disabled', () => {
|
||||||
|
it('should not render neither the upsell, nor the insights for alerts by process ancestry', () => {
|
||||||
|
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestProviders>
|
||||||
|
<Insights browserFields={{}} eventId="test" data={data} timelineId="" />
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('related-alerts-by-ancestry')).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('link', { name: new RegExp(i18n.ALERT_UPSELL) })
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,12 +13,15 @@ import * as i18n from './translations';
|
||||||
|
|
||||||
import type { BrowserFields } from '../../../containers/source';
|
import type { BrowserFields } from '../../../containers/source';
|
||||||
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
|
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
|
||||||
|
import { hasData } from './helpers';
|
||||||
import { useGetUserCasesPermissions } from '../../../lib/kibana';
|
import { useGetUserCasesPermissions } from '../../../lib/kibana';
|
||||||
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
||||||
|
import { useLicense } from '../../../hooks/use_license';
|
||||||
import { RelatedAlertsByProcessAncestry } from './related_alerts_by_process_ancestry';
|
import { RelatedAlertsByProcessAncestry } from './related_alerts_by_process_ancestry';
|
||||||
import { RelatedCases } from './related_cases';
|
import { RelatedCases } from './related_cases';
|
||||||
import { RelatedAlertsBySourceEvent } from './related_alerts_by_source_event';
|
import { RelatedAlertsBySourceEvent } from './related_alerts_by_source_event';
|
||||||
import { RelatedAlertsBySession } from './related_alerts_by_session';
|
import { RelatedAlertsBySession } from './related_alerts_by_session';
|
||||||
|
import { RelatedAlertsUpsell } from './related_alerts_upsell';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
browserFields: BrowserFields;
|
browserFields: BrowserFields;
|
||||||
|
@ -36,6 +39,7 @@ export const Insights = React.memo<Props>(
|
||||||
const isRelatedAlertsByProcessAncestryEnabled = useIsExperimentalFeatureEnabled(
|
const isRelatedAlertsByProcessAncestryEnabled = useIsExperimentalFeatureEnabled(
|
||||||
'insightsRelatedAlertsByProcessAncestry'
|
'insightsRelatedAlertsByProcessAncestry'
|
||||||
);
|
);
|
||||||
|
const hasAtLeastPlatinum = useLicense().isPlatinumPlus();
|
||||||
const processEntityField = find({ category: 'process', field: 'process.entity_id' }, data);
|
const processEntityField = find({ category: 'process', field: 'process.entity_id' }, data);
|
||||||
const originalDocumentId = find(
|
const originalDocumentId = find(
|
||||||
{ category: 'kibana', field: 'kibana.alert.ancestors.id' },
|
{ category: 'kibana', field: 'kibana.alert.ancestors.id' },
|
||||||
|
@ -45,20 +49,20 @@ export const Insights = React.memo<Props>(
|
||||||
{ category: 'kibana', field: 'kibana.alert.rule.parameters.index' },
|
{ category: 'kibana', field: 'kibana.alert.rule.parameters.index' },
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
const hasProcessEntityInfo =
|
const hasProcessEntityInfo = hasData(processEntityField);
|
||||||
isRelatedAlertsByProcessAncestryEnabled && processEntityField && processEntityField.values;
|
|
||||||
|
|
||||||
const processSessionField = find(
|
const processSessionField = find(
|
||||||
{ category: 'process', field: 'process.entry_leader.entity_id' },
|
{ category: 'process', field: 'process.entry_leader.entity_id' },
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
const hasProcessSessionInfo = processSessionField && processSessionField.values;
|
const hasProcessSessionInfo =
|
||||||
|
isRelatedAlertsByProcessAncestryEnabled && hasData(processSessionField);
|
||||||
|
|
||||||
const sourceEventField = find(
|
const sourceEventField = find(
|
||||||
{ category: 'kibana', field: 'kibana.alert.original_event.id' },
|
{ category: 'kibana', field: 'kibana.alert.original_event.id' },
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
const hasSourceEventInfo = sourceEventField && sourceEventField.values;
|
const hasSourceEventInfo = hasData(sourceEventField);
|
||||||
|
|
||||||
const userCasesPermissions = useGetUserCasesPermissions();
|
const userCasesPermissions = useGetUserCasesPermissions();
|
||||||
const hasCasesReadPermissions = userCasesPermissions.read;
|
const hasCasesReadPermissions = userCasesPermissions.read;
|
||||||
|
@ -74,8 +78,7 @@ export const Insights = React.memo<Props>(
|
||||||
|
|
||||||
const canShowAncestryInsight =
|
const canShowAncestryInsight =
|
||||||
isRelatedAlertsByProcessAncestryEnabled &&
|
isRelatedAlertsByProcessAncestryEnabled &&
|
||||||
processEntityField &&
|
hasProcessEntityInfo &&
|
||||||
processEntityField.values &&
|
|
||||||
originalDocumentId &&
|
originalDocumentId &&
|
||||||
originalDocumentIndex;
|
originalDocumentIndex;
|
||||||
|
|
||||||
|
@ -122,17 +125,22 @@ export const Insights = React.memo<Props>(
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canShowAncestryInsight && (
|
{canShowAncestryInsight &&
|
||||||
<EuiFlexItem data-test-subj="related-alerts-by-ancestry">
|
(hasAtLeastPlatinum ? (
|
||||||
<RelatedAlertsByProcessAncestry
|
<EuiFlexItem data-test-subj="related-alerts-by-ancestry">
|
||||||
data={processEntityField}
|
<RelatedAlertsByProcessAncestry
|
||||||
originalDocumentId={originalDocumentId}
|
data={processEntityField}
|
||||||
index={originalDocumentIndex}
|
originalDocumentId={originalDocumentId}
|
||||||
eventId={eventId}
|
index={originalDocumentIndex}
|
||||||
timelineId={timelineId}
|
eventId={eventId}
|
||||||
/>
|
timelineId={timelineId}
|
||||||
</EuiFlexItem>
|
/>
|
||||||
)}
|
</EuiFlexItem>
|
||||||
|
) : (
|
||||||
|
<EuiFlexItem>
|
||||||
|
<RelatedAlertsUpsell />
|
||||||
|
</EuiFlexItem>
|
||||||
|
))}
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useCallback, useEffect, useState } from 'react';
|
import React, { useMemo, useCallback, useEffect, useState } from 'react';
|
||||||
import { EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
|
import { EuiBetaBadge, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
|
||||||
|
|
||||||
import type { DataProvider } from '../../../../../common/types';
|
import type { DataProvider } from '../../../../../common/types';
|
||||||
import { TimelineId } from '../../../../../common/types/timeline';
|
import { TimelineId } from '../../../../../common/types/timeline';
|
||||||
|
@ -23,6 +23,7 @@ import {
|
||||||
PROCESS_ANCESTRY_EMPTY,
|
PROCESS_ANCESTRY_EMPTY,
|
||||||
PROCESS_ANCESTRY_ERROR,
|
PROCESS_ANCESTRY_ERROR,
|
||||||
} from './translations';
|
} from './translations';
|
||||||
|
import { BETA } from '../../../translations';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: TimelineEventsDetailsItem;
|
data: TimelineEventsDetailsItem;
|
||||||
|
@ -112,6 +113,7 @@ export const RelatedAlertsByProcessAncestry = React.memo<Props>(
|
||||||
}
|
}
|
||||||
renderContent={renderContent}
|
renderContent={renderContent}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
|
extraAction={<EuiBetaBadge size="s" label={BETA} color="subdued" />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { EuiSpacer } from '@elastic/eui';
|
import { EuiBetaBadge, EuiSpacer } from '@elastic/eui';
|
||||||
|
|
||||||
import type { BrowserFields } from '../../../containers/source';
|
import type { BrowserFields } from '../../../containers/source';
|
||||||
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
|
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
|
||||||
|
@ -19,6 +19,7 @@ import { SimpleAlertTable } from './simple_alert_table';
|
||||||
import { getEnrichedFieldInfo } from '../helpers';
|
import { getEnrichedFieldInfo } from '../helpers';
|
||||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
|
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
|
||||||
import { SESSION_LOADING, SESSION_EMPTY, SESSION_ERROR, SESSION_COUNT } from './translations';
|
import { SESSION_LOADING, SESSION_EMPTY, SESSION_ERROR, SESSION_COUNT } from './translations';
|
||||||
|
import { BETA } from '../../../translations';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
browserFields: BrowserFields;
|
browserFields: BrowserFields;
|
||||||
|
@ -99,6 +100,7 @@ export const RelatedAlertsBySession = React.memo<Props>(
|
||||||
state={state}
|
state={state}
|
||||||
text={getTextFromState(state, count)}
|
text={getTextFromState(state, count)}
|
||||||
renderContent={renderContent}
|
renderContent={renderContent}
|
||||||
|
extraAction={<EuiBetaBadge size="s" label={BETA} color="subdued" />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiIcon, EuiText } from '@elastic/eui';
|
||||||
|
|
||||||
|
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||||
|
import { ALERT_UPSELL } from './translations';
|
||||||
|
|
||||||
|
const UpsellContainer = euiStyled.div`
|
||||||
|
border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = euiStyled(EuiIcon)`
|
||||||
|
margin-right: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RelatedAlertsUpsell = React.memo(() => {
|
||||||
|
return (
|
||||||
|
<UpsellContainer>
|
||||||
|
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<StyledIcon size="m" type="lock" />
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiText size="s">
|
||||||
|
<EuiLink
|
||||||
|
color="subdued"
|
||||||
|
href="https://www.elastic.co/pricing/"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{ALERT_UPSELL}
|
||||||
|
</EuiLink>
|
||||||
|
</EuiText>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</UpsellContainer>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RelatedAlertsUpsell.displayName = 'RelatedAlertsUpsell';
|
|
@ -135,3 +135,10 @@ export const SIMPLE_ALERT_TABLE_LIMITED = i18n.translate(
|
||||||
defaultMessage: 'Showing only the latest 10 alerts. View the rest of alerts in timeline.',
|
defaultMessage: 'Showing only the latest 10 alerts. View the rest of alerts in timeline.',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ALERT_UPSELL = i18n.translate(
|
||||||
|
'xpack.securitySolution.alertDetails.overview.insights.alertUpsellTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Get more insights with a subscription',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -23,14 +23,14 @@ export const registerResolverRoutes = async (
|
||||||
startServices: StartServicesAccessor<StartPlugins>,
|
startServices: StartServicesAccessor<StartPlugins>,
|
||||||
config: ConfigType
|
config: ConfigType
|
||||||
) => {
|
) => {
|
||||||
const [, { ruleRegistry }] = await startServices();
|
const [, { ruleRegistry, licensing }] = await startServices();
|
||||||
router.post(
|
router.post(
|
||||||
{
|
{
|
||||||
path: '/api/endpoint/resolver/tree',
|
path: '/api/endpoint/resolver/tree',
|
||||||
validate: validateTree,
|
validate: validateTree,
|
||||||
options: { authRequired: true },
|
options: { authRequired: true },
|
||||||
},
|
},
|
||||||
handleTree(ruleRegistry, config)
|
handleTree(ruleRegistry, config, licensing)
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
|
|
|
@ -5,23 +5,37 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import type { RequestHandler } from '@kbn/core/server';
|
import type { RequestHandler } from '@kbn/core/server';
|
||||||
import type { TypeOf } from '@kbn/config-schema';
|
import type { TypeOf } from '@kbn/config-schema';
|
||||||
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
|
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
|
||||||
import type { validateTree } from '../../../../../common/endpoint/schema/resolver';
|
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
|
||||||
import type { ConfigType } from '../../../../config';
|
import type { ConfigType } from '../../../../config';
|
||||||
|
|
||||||
|
import type { validateTree } from '../../../../../common/endpoint/schema/resolver';
|
||||||
|
import { featureUsageService } from '../../../services/feature_usage';
|
||||||
import { Fetcher } from './utils/fetch';
|
import { Fetcher } from './utils/fetch';
|
||||||
|
|
||||||
export function handleTree(
|
export function handleTree(
|
||||||
ruleRegistry: RuleRegistryPluginStartContract,
|
ruleRegistry: RuleRegistryPluginStartContract,
|
||||||
config: ConfigType
|
config: ConfigType,
|
||||||
|
licensing: LicensingPluginStart
|
||||||
): RequestHandler<unknown, unknown, TypeOf<typeof validateTree.body>> {
|
): RequestHandler<unknown, unknown, TypeOf<typeof validateTree.body>> {
|
||||||
return async (context, req, res) => {
|
return async (context, req, res) => {
|
||||||
const client = (await context.core).elasticsearch.client;
|
const client = (await context.core).elasticsearch.client;
|
||||||
const {
|
const {
|
||||||
experimentalFeatures: { insightsRelatedAlertsByProcessAncestry },
|
experimentalFeatures: { insightsRelatedAlertsByProcessAncestry },
|
||||||
} = config;
|
} = config;
|
||||||
const alertsClient = insightsRelatedAlertsByProcessAncestry
|
const license = await firstValueFrom(licensing.license$);
|
||||||
|
const hasAccessToInsightsRelatedByProcessAncestry =
|
||||||
|
insightsRelatedAlertsByProcessAncestry && license.hasAtLeast('platinum');
|
||||||
|
|
||||||
|
if (hasAccessToInsightsRelatedByProcessAncestry) {
|
||||||
|
featureUsageService.notifyUsage('ALERTS_BY_PROCESS_ANCESTRY');
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertsClient = hasAccessToInsightsRelatedByProcessAncestry
|
||||||
? await ruleRegistry.getRacClientWithRequest(req)
|
? await ruleRegistry.getRacClientWithRequest(req)
|
||||||
: undefined;
|
: undefined;
|
||||||
const fetcher = new Fetcher(client, alertsClient);
|
const fetcher = new Fetcher(client, alertsClient);
|
||||||
|
|
|
@ -21,6 +21,7 @@ const FEATURES = {
|
||||||
KILL_PROCESS: 'Kill process',
|
KILL_PROCESS: 'Kill process',
|
||||||
SUSPEND_PROCESS: 'Suspend process',
|
SUSPEND_PROCESS: 'Suspend process',
|
||||||
RUNNING_PROCESSES: 'Get running processes',
|
RUNNING_PROCESSES: 'Get running processes',
|
||||||
|
ALERTS_BY_PROCESS_ANCESTRY: 'Get related alerts by process ancestry',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type FeatureKeys = keyof typeof FEATURES;
|
export type FeatureKeys = keyof typeof FEATURES;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue