/* * 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 reduceReducers from 'reduce-reducers'; import type { Subscription } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs'; import { combineLatestWith, pluck } from 'rxjs/operators'; import type { AnyAction, Reducer } from 'redux'; import type { AppMountParameters, AppUpdater, CoreSetup, CoreStart, PluginInitializerContext, Plugin as IPlugin, } from '@kbn/core/public'; import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '@kbn/core/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { TimelineState } from '@kbn/timelines-plugin/public'; import type { PluginSetup, PluginStart, SetupPlugins, StartPlugins, StartServices, AppObservableLibs, SubPlugins, StartedSubPlugins, StartPluginsDependencies, } from './types'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; import { SOLUTION_NAME } from './common/translations'; import { APP_ID, APP_UI_ID, APP_PATH, DEFAULT_INDEX_KEY, APP_ICON_SOLUTION, DETECTION_ENGINE_INDEX_URL, SERVER_APP_ID, SOURCERER_API_URL, ENABLE_GROUPED_NAVIGATION, } from '../common/constants'; import { getDeepLinks, registerDeepLinksUpdater } from './app/deep_links'; import type { LinksPermissions } from './common/links'; import { updateAppLinks } from './common/links'; import { getSubPluginRoutesByCapabilities, manageOldSiemRoutes } from './helpers'; import type { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; import type { SecuritySolutionUiConfigType } from './common/types'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; import { getLazyEndpointPolicyResponseExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_response_extension'; import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; import type { SourcererModel, KibanaDataView } from './common/store/sourcerer/model'; import { initDataView } from './common/store/sourcerer/model'; import type { SecurityDataView } from './common/containers/sourcerer/api'; export class Plugin implements IPlugin { readonly kibanaVersion: string; private config: SecuritySolutionUiConfigType; readonly experimentalFeatures: ExperimentalFeatures; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []); this.kibanaVersion = initializerContext.env.packageInfo.version; } private appUpdater$ = new Subject(); private storage = new Storage(localStorage); /** * Lazily instantiated subPlugins. * See `subPlugins` method. */ private _subPlugins?: SubPlugins; /** * Lazily instantiated `SecurityAppStore`. * See `store` method. */ private _store?: SecurityAppStore; public setup( core: CoreSetup, plugins: SetupPlugins ): PluginSetup { initTelemetry( { usageCollection: plugins.usageCollection, }, APP_UI_ID ); if (plugins.home) { plugins.home.featureCatalogue.registerSolution({ id: APP_ID, title: SOLUTION_NAME, description: i18n.translate('xpack.securitySolution.featureCatalogueDescription', { defaultMessage: 'Prevent, collect, detect, and respond to threats for unified protection across your infrastructure.', }), icon: 'logoSecurity', path: APP_PATH, order: 300, }); } /** * `StartServices` which are needed by the `renderApp` function when mounting any of the subPlugin applications. * This is a promise because these aren't available until the `start` lifecycle phase but they are referenced * in the `setup` lifecycle phase. */ const startServices: Promise = (async () => { const [coreStart, startPluginsDeps] = await core.getStartServices(); const { apm } = await import('@elastic/apm-rum'); const { savedObjectsTaggingOss, ...startPlugins } = startPluginsDeps; const services: StartServices = { ...coreStart, ...startPlugins, apm, savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(), storage: this.storage, security: plugins.security, }; return services; })(); core.application.register({ id: APP_UI_ID, title: SOLUTION_NAME, appRoute: APP_PATH, category: DEFAULT_APP_CATEGORIES.security, // Initializing app as visible to make sure it appears on the Kibana home page, it is hidden when deepLinks update navLinkStatus: AppNavLinkStatus.visible, searchable: true, updater$: this.appUpdater$, euiIconType: APP_ICON_SOLUTION, mount: async (params: AppMountParameters) => { // required to show the alert table inside cases const { alertsTableConfigurationRegistry } = plugins.triggersActionsUi; const { registerAlertsTableConfiguration } = await this.lazyRegisterAlertsTableConfiguration(); registerAlertsTableConfiguration(alertsTableConfigurationRegistry, this.storage); const [coreStart, startPlugins] = await core.getStartServices(); const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins); const { renderApp } = await this.lazyApplicationDependencies(); return renderApp({ ...params, services: await startServices, store: await this.store(coreStart, startPlugins, subPlugins), usageCollection: plugins.usageCollection, subPluginRoutes: getSubPluginRoutesByCapabilities( subPlugins, coreStart.application.capabilities ), }); }, }); core.application.register({ id: 'siem', appRoute: 'app/siem', title: 'SIEM', navLinkStatus: 3, mount: async (params: AppMountParameters) => { const [coreStart] = await core.getStartServices(); const subscription = this.appUpdater$.subscribe(() => { // wait for app initialization to set the links manageOldSiemRoutes(coreStart); subscription.unsubscribe(); }); return () => true; }, }); return { resolver: async () => { /** * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. * See https://webpack.js.org/api/module-methods/#magic-comments */ const { resolverPluginSetup } = await import( /* webpackChunkName: "resolver" */ './resolver' ); return resolverPluginSetup(); }, }; } public start(core: CoreStart, plugins: StartPlugins) { KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion }); ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures }); licenseService.start(plugins.licensing.license$); if (plugins.fleet) { const { registerExtension } = plugins.fleet; registerExtension({ package: 'endpoint', view: 'package-policy-edit', Component: getLazyEndpointPolicyEditExtension(core, plugins), }); if (this.experimentalFeatures.policyResponseInFleetEnabled) { registerExtension({ package: 'endpoint', view: 'package-policy-response', Component: getLazyEndpointPolicyResponseExtension(core, plugins), }); } registerExtension({ package: 'endpoint', view: 'package-policy-create', Component: LazyEndpointPolicyCreateExtension, }); registerExtension({ package: 'endpoint', view: 'package-detail-custom', Component: getLazyEndpointPackageCustomExtension(core, plugins), }); registerExtension({ package: 'endpoint', view: 'package-detail-assets', Component: LazyEndpointCustomAssetsExtension, }); } // Not using await to prevent blocking start execution this.registerAppLinks(core, plugins); return {}; } public stop() { licenseService.stop(); return {}; } /** * The dependencies needed to mount the applications. These are dynamically loaded for the sake of webpack bundling efficiency. * Webpack is smart enough to only request (and download) this even when it is imported multiple times concurrently. */ private lazyApplicationDependencies() { /** * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. * See https://webpack.js.org/api/module-methods/#magic-comments */ return import( /* webpackChunkName: "lazy_application_dependencies" */ './lazy_application_dependencies' ); } /** * The dependencies needed to mount the applications. These are dynamically loaded for the sake of webpack bundling efficiency. * Webpack is smart enough to only request (and download) this even when it is imported multiple times concurrently. */ private lazySubPlugins() { /** * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. * See https://webpack.js.org/api/module-methods/#magic-comments */ return import( /* webpackChunkName: "lazy_sub_plugins" */ './lazy_sub_plugins' ); } private lazyRegisterAlertsTableConfiguration() { /** * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. * See https://webpack.js.org/api/module-methods/#magic-comments */ return import( /* webpackChunkName: "lazy_register_alerts_table_configuration" */ './common/lib/triggers_actions_ui/register_alerts_table_configuration' ); } private lazyApplicationLinks() { /** * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. * See https://webpack.js.org/api/module-methods/#magic-comments */ return import( /* webpackChunkName: "lazy_app_links" */ './common/links/app_links' ); } /** * Lazily instantiated subPlugins. This should be instantiated just once. */ private async subPlugins(): Promise { if (!this._subPlugins) { const { subPluginClasses } = await this.lazySubPlugins(); this._subPlugins = { alerts: new subPluginClasses.Detections(), rules: new subPluginClasses.Rules(), exceptions: new subPluginClasses.Exceptions(), cases: new subPluginClasses.Cases(), hosts: new subPluginClasses.Hosts(), users: new subPluginClasses.Users(), network: new subPluginClasses.Network(), kubernetes: new subPluginClasses.Kubernetes(), overview: new subPluginClasses.Overview(), timelines: new subPluginClasses.Timelines(), management: new subPluginClasses.Management(), landingPages: new subPluginClasses.LandingPages(), cloudSecurityPosture: new subPluginClasses.CloudSecurityPosture(), threatIntelligence: new subPluginClasses.ThreatIntelligence(), }; } return this._subPlugins; } /** * All started subPlugins. */ private async startSubPlugins( storage: Storage, core: CoreStart, plugins: StartPlugins ): Promise { const subPlugins = await this.subPlugins(); return { overview: subPlugins.overview.start(), alerts: subPlugins.alerts.start(storage), cases: subPlugins.cases.start(), rules: subPlugins.rules.start(storage), exceptions: subPlugins.exceptions.start(storage), hosts: subPlugins.hosts.start(storage), users: subPlugins.users.start(storage), network: subPlugins.network.start(storage), timelines: subPlugins.timelines.start(), kubernetes: subPlugins.kubernetes.start(), management: subPlugins.management.start(core, plugins), landingPages: subPlugins.landingPages.start(), cloudSecurityPosture: subPlugins.cloudSecurityPosture.start(), threatIntelligence: subPlugins.threatIntelligence.start(), }; } /** * Lazily instantiate a `SecurityAppStore`. We lazily instantiate this because it requests large dynamic imports. We instantiate it once because each subPlugin needs to share the same reference. */ private async store( coreStart: CoreStart, startPlugins: StartPlugins, subPlugins: StartedSubPlugins ): Promise { if (!this._store) { let signal: { name: string | null } = { name: null }; try { if (coreStart.application.capabilities[SERVER_APP_ID].show === true) { signal = await coreStart.http.fetch(DETECTION_ENGINE_INDEX_URL, { method: 'GET', }); } } catch { signal = { name: null }; } const configPatternList = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); let defaultDataView: SourcererModel['defaultDataView']; let kibanaDataViews: SourcererModel['kibanaDataViews']; try { // check for/generate default Security Solution Kibana data view const sourcererDataViews: SecurityDataView = await coreStart.http.fetch(SOURCERER_API_URL, { method: 'POST', body: JSON.stringify({ patternList: [...configPatternList, ...(signal.name != null ? [signal.name] : [])], }), }); defaultDataView = { ...initDataView, ...sourcererDataViews.defaultDataView }; kibanaDataViews = sourcererDataViews.kibanaDataViews.map((dataView: KibanaDataView) => ({ ...initDataView, ...dataView, })); } catch (error) { defaultDataView = { ...initDataView, error }; kibanaDataViews = []; } const { createStore, createInitialState } = await this.lazyApplicationDependencies(); const appLibs: AppObservableLibs = { kibana: coreStart }; const libs$ = new BehaviorSubject(appLibs); const timelineInitialState = { timeline: { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...subPlugins.timelines.store.initialState.timeline!, timelineById: { ...subPlugins.timelines.store.initialState.timeline.timelineById, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...subPlugins.alerts.storageTimelines!.timelineById, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...subPlugins.rules.storageTimelines!.timelineById, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...subPlugins.exceptions.storageTimelines!.timelineById, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...subPlugins.hosts.storageTimelines!.timelineById, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...subPlugins.network.storageTimelines!.timelineById, }, }, }; const tGridReducer = startPlugins.timelines?.getTGridReducer() ?? {}; const timelineReducer = reduceReducers( timelineInitialState.timeline, tGridReducer, subPlugins.timelines.store.reducer.timeline ) as unknown as Reducer; this._store = createStore( createInitialState( { ...subPlugins.hosts.store.initialState, ...subPlugins.users.store.initialState, ...subPlugins.network.store.initialState, ...timelineInitialState, ...subPlugins.management.store.initialState, }, { defaultDataView, kibanaDataViews, signalIndexName: signal.name, enableExperimental: this.experimentalFeatures, } ), { ...subPlugins.hosts.store.reducer, ...subPlugins.users.store.reducer, ...subPlugins.network.store.reducer, timeline: timelineReducer, ...subPlugins.management.store.reducer, ...tGridReducer, }, libs$.pipe(pluck('kibana')), this.storage, [...(subPlugins.management.store.middleware ?? [])] ); } if (startPlugins.timelines) { startPlugins.timelines.setTGridEmbeddedStore(this._store); } return this._store; } /** * Register deepLinks and appUpdater for all app links, to change deepLinks as needed when licensing changes. */ async registerAppLinks(core: CoreStart, plugins: StartPlugins) { const { links, getFilteredLinks } = await this.lazyApplicationLinks(); const { license$ } = plugins.licensing; const newNavEnabled$ = core.uiSettings.get$(ENABLE_GROUPED_NAVIGATION, true); let appLinksSubscription: Subscription | null = null; license$.pipe(combineLatestWith(newNavEnabled$)).subscribe(async ([license, newNavEnabled]) => { const linksPermissions: LinksPermissions = { experimentalFeatures: this.experimentalFeatures, capabilities: core.application.capabilities, }; if (license.type !== undefined) { linksPermissions.license = license; } if (appLinksSubscription) { appLinksSubscription.unsubscribe(); appLinksSubscription = null; } if (newNavEnabled) { appLinksSubscription = registerDeepLinksUpdater(this.appUpdater$); } else { // old nav links update this.appUpdater$.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden, deepLinks: getDeepLinks( this.experimentalFeatures, license.type, core.application.capabilities ), })); } // set initial links to not block rendering updateAppLinks(links, linksPermissions); // set filtered links asynchronously const filteredLinks = await getFilteredLinks(core, plugins); updateAppLinks(filteredLinks, linksPermissions); }); } }