mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
* [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>
520 lines
19 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
}
|