kibana/x-pack/plugins/security_solution/public/plugin.tsx
Philippe Oberti 19aa51e5a8
[TIP] Add new Threat intelligence plugin (#136479)
* [TIP] Add Threat Intelligence plugin

- create Threat Intelligence plugin and integrate with Security Solution plugin
- setup jest unit tests, i18n, Cypress tests and  Storybook
- fetch Indicator of Compromise, and display in data-grid
- add flyout components to show IOCs details (table and JSON)
- add new threatIntelInt entry to kbn-doc-links package

https://github.com/elastic/security-team/issues/4329
https://github.com/elastic/security-team/issues/4138
https://github.com/elastic/security-team/issues/4241
https://github.com/elastic/security-team/issues/4242
https://github.com/elastic/security-team/issues/4244
https://github.com/elastic/security-team/issues/4245

Co-authored-by: lgmys <lgmys@pm.me>
Co-authored-by: Maxim Kholod <maxim.kholod@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
2022-07-25 19:27:05 +02:00

520 lines
19 KiB
TypeScript

/*
* 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<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
readonly kibanaVersion: string;
private config: SecuritySolutionUiConfigType;
readonly experimentalFeatures: ExperimentalFeatures;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get<SecuritySolutionUiConfigType>();
this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []);
this.kibanaVersion = initializerContext.env.packageInfo.version;
}
private appUpdater$ = new Subject<AppUpdater>();
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<StartPluginsDependencies, PluginStart>,
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<StartServices> = (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<SubPlugins> {
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<StartedSubPlugins> {
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<SecurityAppStore> {
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<TimelineState, AnyAction>;
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$<boolean>(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);
});
}
}