Skip to content

Commit

Permalink
[Security Solution] Add searchDeepLinks to security solution (#89772) (
Browse files Browse the repository at this point in the history
…#91788)

* WIP add search for siem entities

* Remove frontend search provider, use searchDeepLinks from core

* Comment and use both functions used to generate subPlugin meta information

* Use correct url for timeline templates

* Remove case management attibute from saved object type

* Remove unused globalSearch plugin from kibana.json

* Add comments, use Subject instead of BehaviorSubject for appUpdaters

* Unsubscribe from license on plugin stop
  • Loading branch information
kqualters-elastic authored Feb 18, 2021
1 parent 08fc722 commit 24d0e3d
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 4 deletions.
23 changes: 23 additions & 0 deletions x-pack/plugins/security_solution/public/app/search/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { getSearchDeepLinksAndKeywords } from '.';
import { SecurityPageName } from '../../../common/constants';

describe('public search functions', () => {
it('returns a subset of links for basic license, full set for platinum', () => {
const basicLicense = 'basic';
const platinumLicense = 'platinum';
for (const pageName of Object.values(SecurityPageName)) {
expect.assertions(Object.values(SecurityPageName).length * 2);
const basicLinkCount =
getSearchDeepLinksAndKeywords(pageName, basicLicense).searchDeepLinks?.length || 0;
const platinumLinks = getSearchDeepLinksAndKeywords(pageName, platinumLicense);
expect(platinumLinks.searchDeepLinks?.length).toBeGreaterThanOrEqual(basicLinkCount);
expect(platinumLinks.keywords?.length).not.toBe(null);
}
});
});
234 changes: 234 additions & 0 deletions x-pack/plugins/security_solution/public/app/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { Subject } from 'rxjs';

import { AppUpdater } from 'src/core/public';
import { LicenseType } from '../../../../licensing/common/types';
import { SecuritySubPluginNames, SecurityDeepLinks } from '../types';
import { AppMeta } from '../../../../../../src/core/public';

const securityDeepLinks: SecurityDeepLinks = {
detections: {
base: [
{
id: 'siemDetectionRules',
title: i18n.translate('xpack.securitySolution.search.detections.manage', {
defaultMessage: 'Manage Rules',
}),
keywords: ['rules'],
path: '/rules',
},
],
},
hosts: {
base: [
{
id: 'authentications',
title: i18n.translate('xpack.securitySolution.search.hosts.authentications', {
defaultMessage: 'Authentications',
}),
path: '/authentications',
},
{
id: 'uncommonProcesses',
title: i18n.translate('xpack.securitySolution.search.hosts.uncommonProcesses', {
defaultMessage: 'Uncommon Processes',
}),
path: '/uncommonProcesses',
},
{
id: 'events',
title: i18n.translate('xpack.securitySolution.search.hosts.events', {
defaultMessage: 'Events',
}),
path: '/events',
},
{
id: 'externalAlerts',
title: i18n.translate('xpack.securitySolution.search.hosts.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: '/alerts',
},
],
premium: [
{
id: 'anomalies',
title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', {
defaultMessage: 'Anomalies',
}),
path: '/anomalies',
},
],
},
network: {
base: [
{
id: 'dns',
title: i18n.translate('xpack.securitySolution.search.network.dns', {
defaultMessage: 'DNS',
}),
path: '/dns',
},
{
id: 'http',
title: i18n.translate('xpack.securitySolution.search.network.http', {
defaultMessage: 'HTTP',
}),
path: '/http',
},
{
id: 'tls',
title: i18n.translate('xpack.securitySolution.search.network.tls', {
defaultMessage: 'TLS',
}),
path: '/tls',
},
{
id: 'externalAlertsNetwork',
title: i18n.translate('xpack.securitySolution.search.network.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: '/external-alerts',
},
],
premium: [
{
id: 'anomalies',
title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', {
defaultMessage: 'Anomalies',
}),
path: '/anomalies',
},
],
},
timelines: {
base: [
{
id: 'timelineTemplates',
title: i18n.translate('xpack.securitySolution.search.timeline.templates', {
defaultMessage: 'Templates',
}),
path: '/template',
},
],
},
overview: {
base: [],
},
case: {
base: [],
premium: [
{
id: 'configure',
title: i18n.translate('xpack.securitySolution.search.cases.configure', {
defaultMessage: 'Configure Cases',
}),
path: '/configure',
},
],
},
administration: {
base: [
{
id: 'trustApplications',
title: i18n.translate('xpack.securitySolution.search.administration.trustedApps', {
defaultMessage: 'Trusted Applications',
}),
path: '/trusted_apps',
},
],
},
};

const subpluginKeywords: { [key in SecuritySubPluginNames]: string[] } = {
detections: [
i18n.translate('xpack.securitySolution.search.detections', {
defaultMessage: 'Detections',
}),
],
hosts: [
i18n.translate('xpack.securitySolution.search.hosts', {
defaultMessage: 'Hosts',
}),
],
network: [
i18n.translate('xpack.securitySolution.search.network', {
defaultMessage: 'Network',
}),
],
timelines: [
i18n.translate('xpack.securitySolution.search.timelines', {
defaultMessage: 'Timelines',
}),
],
overview: [
i18n.translate('xpack.securitySolution.search.overview', {
defaultMessage: 'Overview',
}),
],
case: [
i18n.translate('xpack.securitySolution.search.cases', {
defaultMessage: 'Cases',
}),
],
administration: [
i18n.translate('xpack.securitySolution.search.administration', {
defaultMessage: 'Endpoint Administration',
}),
],
};

/**
* A function that generates a subPlugin's meta tag
* @param subPluginName SubPluginName of the app to retrieve meta information for.
* @param licenseType optional string for license level, if not provided basic is assumed.
*/
export function getSearchDeepLinksAndKeywords(
subPluginName: SecuritySubPluginNames,
licenseType?: LicenseType
): AppMeta {
const baseRoutes = [...securityDeepLinks[subPluginName].base];
if (
licenseType === 'gold' ||
licenseType === 'platinum' ||
licenseType === 'enterprise' ||
licenseType === 'trial'
) {
const premiumRoutes =
securityDeepLinks[subPluginName] && securityDeepLinks[subPluginName].premium;
if (premiumRoutes !== undefined) {
return {
keywords: subpluginKeywords[subPluginName],
searchDeepLinks: [...baseRoutes, ...premiumRoutes],
};
}
}
return {
keywords: subpluginKeywords[subPluginName],
searchDeepLinks: baseRoutes,
};
}
/**
* A function that updates a subplugin's meta property as appropriate when license level changes.
* @param subPluginName SubPluginName of the app to register searchDeepLinks for
* @param appUpdater an instance of appUpdater$ observable to update search links when needed.
* @param licenseType A string representing the current license level.
*/
export function registerSearchLinks(
subPluginName: SecuritySubPluginNames,
appUpdater?: Subject<AppUpdater>,
licenseType?: LicenseType
) {
if (appUpdater !== undefined) {
appUpdater.next(() => ({
meta: getSearchDeepLinksAndKeywords(subPluginName, licenseType),
}));
}
}
12 changes: 11 additions & 1 deletion x-pack/plugins/security_solution/public/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
CombinedState,
} from 'redux';

import { AppMountParameters } from '../../../../../src/core/public';
import { AppMountParameters, AppSearchDeepLink } from '../../../../../src/core/public';
import { StartServices } from '../types';
import { AppFrontendLibs } from '../common/lib/lib';

Expand All @@ -34,6 +34,7 @@ import { State, SubPluginsInitReducer } from '../common/store';
import { Immutable } from '../../common/endpoint/types';
import { AppAction } from '../common/store/actions';
import { TimelineState } from '../timelines/store/timeline/types';
import { SecurityPageName } from '../../common/constants';
export { SecurityPageName } from '../../common/constants';

export interface SecuritySubPluginStore<K extends SecuritySubPluginKeyStore, T> {
Expand All @@ -55,6 +56,15 @@ export type SecuritySubPluginKeyStore =
| 'alertList'
| 'management';

export type SecuritySubPluginNames = keyof typeof SecurityPageName;

interface SecurityDeepLink {
base: AppSearchDeepLink[];
premium?: AppSearchDeepLink[];
}

export type SecurityDeepLinks = { [key in SecuritySubPluginNames]: SecurityDeepLink };

/**
* Returned by the various 'SecuritySubPlugin' classes from the `start` method.
*/
Expand Down
33 changes: 31 additions & 2 deletions x-pack/plugins/security_solution/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { i18n } from '@kbn/i18n';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
import { pluck } from 'rxjs/operators';
import {
PluginSetup,
Expand All @@ -19,6 +19,7 @@ import {
} from './types';
import {
AppMountParameters,
AppUpdater,
CoreSetup,
CoreStart,
PluginInitializerContext,
Expand Down Expand Up @@ -46,6 +47,7 @@ import {
} from '../common/constants';

import { SecurityPageName } from './app/types';
import { registerSearchLinks, getSearchDeepLinksAndKeywords } from './app/search';
import { manageOldSiemRoutes } from './helpers';
import {
OVERVIEW,
Expand Down Expand Up @@ -73,8 +75,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
constructor(initializerContext: PluginInitializerContext) {
this.kibanaVersion = initializerContext.env.packageInfo.version;
}
private detectionsUpdater$ = new Subject<AppUpdater>();
private hostsUpdater$ = new Subject<AppUpdater>();
private networkUpdater$ = new Subject<AppUpdater>();

private storage = new Storage(localStorage);
private licensingSubscription: Subscription | null = null;

/**
* Lazily instantiated subPlugins.
Expand Down Expand Up @@ -184,6 +190,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
euiIconType: APP_ICON_SOLUTION,
category: DEFAULT_APP_CATEGORIES.security,
appRoute: APP_DETECTIONS_PATH,
updater$: this.detectionsUpdater$,
mount: async (params: AppMountParameters) => {
const [coreStart, startPlugins] = await core.getStartServices();
const { detections: subPlugin } = await this.subPlugins();
Expand All @@ -206,6 +213,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
euiIconType: APP_ICON_SOLUTION,
category: DEFAULT_APP_CATEGORIES.security,
appRoute: APP_HOSTS_PATH,
updater$: this.hostsUpdater$,
mount: async (params: AppMountParameters) => {
const [coreStart, startPlugins] = await core.getStartServices();
const { hosts: subPlugin } = await this.subPlugins();
Expand All @@ -227,6 +235,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
euiIconType: APP_ICON_SOLUTION,
category: DEFAULT_APP_CATEGORIES.security,
appRoute: APP_NETWORK_PATH,
updater$: this.networkUpdater$,
mount: async (params: AppMountParameters) => {
const [coreStart, startPlugins] = await core.getStartServices();
const { network: subPlugin } = await this.subPlugins();
Expand All @@ -248,6 +257,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
euiIconType: APP_ICON_SOLUTION,
category: DEFAULT_APP_CATEGORIES.security,
appRoute: APP_TIMELINES_PATH,
meta: getSearchDeepLinksAndKeywords(SecurityPageName.timelines),
mount: async (params: AppMountParameters) => {
const [coreStart, startPlugins] = await core.getStartServices();
const { timelines: subPlugin } = await this.subPlugins();
Expand Down Expand Up @@ -356,11 +366,30 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
});
}
licenseService.start(plugins.licensing.license$);

const licensing = licenseService.getLicenseInformation$();
/**
* Register searchDeepLinks and pass an appUpdater for each subPlugin, to change searchDeepLinks as needed when licensing changes.
*/
if (licensing !== null) {
this.licensingSubscription = licensing.subscribe((currentLicense) => {
if (currentLicense.type !== undefined) {
registerSearchLinks(SecurityPageName.network, this.networkUpdater$, currentLicense.type);
registerSearchLinks(
SecurityPageName.detections,
this.detectionsUpdater$,
currentLicense.type
);
registerSearchLinks(SecurityPageName.hosts, this.hostsUpdater$, currentLicense.type);
}
});
}
return {};
}

public stop() {
if (this.licensingSubscription !== null) {
this.licensingSubscription.unsubscribe();
}
return {};
}

Expand Down
Loading

0 comments on commit 24d0e3d

Please sign in to comment.