From 24d0e3d7be2ea3769d4cb7d6b42222061bd5a2d1 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 18 Feb 2021 00:12:06 -0500 Subject: [PATCH] [Security Solution] Add searchDeepLinks to security solution (#89772) (#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 --- .../public/app/search/index.test.ts | 23 ++ .../public/app/search/index.ts | 234 ++++++++++++++++++ .../security_solution/public/app/types.ts | 12 +- .../security_solution/public/plugin.tsx | 33 ++- .../plugins/security_solution/public/types.ts | 3 +- 5 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/app/search/index.test.ts create mode 100644 x-pack/plugins/security_solution/public/app/search/index.ts diff --git a/x-pack/plugins/security_solution/public/app/search/index.test.ts b/x-pack/plugins/security_solution/public/app/search/index.test.ts new file mode 100644 index 00000000000000..d6c36e89558d02 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/search/index.test.ts @@ -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); + } + }); +}); diff --git a/x-pack/plugins/security_solution/public/app/search/index.ts b/x-pack/plugins/security_solution/public/app/search/index.ts new file mode 100644 index 00000000000000..bddb43588bb6d1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/search/index.ts @@ -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, + licenseType?: LicenseType +) { + if (appUpdater !== undefined) { + appUpdater.next(() => ({ + meta: getSearchDeepLinksAndKeywords(subPluginName, licenseType), + })); + } +} diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 4d1c091fdaa5fd..95e64fe37d3331 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -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'; @@ -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 { @@ -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. */ diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 1b7b7059cbafce..e8997cddc2cdcd 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -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, @@ -19,6 +19,7 @@ import { } from './types'; import { AppMountParameters, + AppUpdater, CoreSetup, CoreStart, PluginInitializerContext, @@ -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, @@ -73,8 +75,12 @@ export class Plugin implements IPlugin(); + private hostsUpdater$ = new Subject(); + private networkUpdater$ = new Subject(); private storage = new Storage(localStorage); + private licensingSubscription: Subscription | null = null; /** * Lazily instantiated subPlugins. @@ -184,6 +190,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { detections: subPlugin } = await this.subPlugins(); @@ -206,6 +213,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { hosts: subPlugin } = await this.subPlugins(); @@ -227,6 +235,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { network: subPlugin } = await this.subPlugins(); @@ -248,6 +257,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { timelines: subPlugin } = await this.subPlugins(); @@ -356,11 +366,30 @@ export class Plugin implements IPlugin { + 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 {}; } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 7a9e9f07b20627..e88077679e1b62 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -34,10 +34,11 @@ import { Network } from './network'; import { Overview } from './overview'; import { Timelines } from './timelines'; import { Management } from './management'; -import { LicensingPluginStart } from '../../licensing/public'; +import { LicensingPluginStart, LicensingPluginSetup } from '../../licensing/public'; export interface SetupPlugins { home?: HomePublicPluginSetup; + licensing: LicensingPluginSetup; security: SecurityPluginSetup; triggersActionsUi: TriggersActionsSetup; usageCollection?: UsageCollectionSetup;