diff --git a/x-pack/plugins/endpoint/kibana.json b/x-pack/plugins/endpoint/kibana.json
index a7fd20b93f62d..f7a4acd629324 100644
--- a/x-pack/plugins/endpoint/kibana.json
+++ b/x-pack/plugins/endpoint/kibana.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "endpoint"],
- "requiredPlugins": ["embeddable"],
+ "requiredPlugins": ["features", "embeddable"],
"server": true,
"ui": true
}
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
new file mode 100644
index 0000000000000..1af27f039aca7
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as React from 'react';
+import ReactDOM from 'react-dom';
+import { CoreStart, AppMountParameters } from 'kibana/public';
+import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
+
+/**
+ * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle.
+ */
+export function renderApp(coreStart: CoreStart, { element }: AppMountParameters) {
+ coreStart.http.get('/api/endpoint/hello-world');
+
+ ReactDOM.render(, element);
+
+ return () => {
+ ReactDOM.unmountComponentAtNode(element);
+ };
+}
+
+const AppRoot = React.memo(() => (
+
+
+
+
+
+));
diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts
index 21bf1b3cdea12..02514cc974af0 100644
--- a/x-pack/plugins/endpoint/public/plugin.ts
+++ b/x-pack/plugins/endpoint/public/plugin.ts
@@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Plugin, CoreSetup } from 'kibana/public';
+import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public';
import { IEmbeddableSetup } from 'src/plugins/embeddable/public';
+import { i18n } from '@kbn/i18n';
import { ResolverEmbeddableFactory } from './embeddables/resolver';
export type EndpointPluginStart = void;
@@ -24,8 +25,20 @@ export class EndpointPlugin
EndpointPluginSetupDependencies,
EndpointPluginStartDependencies
> {
- public setup(_core: CoreSetup, plugins: EndpointPluginSetupDependencies) {
+ public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) {
const resolverEmbeddableFactory = new ResolverEmbeddableFactory();
+ core.application.register({
+ id: 'endpoint',
+ title: i18n.translate('xpack.endpoint.pluginTitle', {
+ defaultMessage: 'Endpoint',
+ }),
+ async mount(params: AppMountParameters) {
+ const [coreStart] = await core.getStartServices();
+ const { renderApp } = await import('./applications/endpoint');
+ return renderApp(coreStart, params);
+ },
+ });
+
plugins.embeddable.registerEmbeddableFactory(
resolverEmbeddableFactory.type,
resolverEmbeddableFactory
diff --git a/x-pack/plugins/endpoint/server/index.ts b/x-pack/plugins/endpoint/server/index.ts
index f10bc7ee51b2c..eec836141ea5e 100644
--- a/x-pack/plugins/endpoint/server/index.ts
+++ b/x-pack/plugins/endpoint/server/index.ts
@@ -19,8 +19,8 @@ export const config = {
};
export const plugin: PluginInitializer<
- EndpointPluginStart,
EndpointPluginSetup,
- EndpointPluginStartDependencies,
- EndpointPluginSetupDependencies
+ EndpointPluginStart,
+ EndpointPluginSetupDependencies,
+ EndpointPluginStartDependencies
> = () => new EndpointPlugin();
diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts
index 400b906c5230e..b41dfee1f78fd 100644
--- a/x-pack/plugins/endpoint/server/plugin.ts
+++ b/x-pack/plugins/endpoint/server/plugin.ts
@@ -3,28 +3,56 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
import { Plugin, CoreSetup } from 'kibana/server';
import { addRoutes } from './routes';
+import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server';
export type EndpointPluginStart = void;
export type EndpointPluginSetup = void;
-export interface EndpointPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface
-
export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface
+export interface EndpointPluginSetupDependencies {
+ features: FeaturesPluginSetupContract;
+}
+
export class EndpointPlugin
implements
Plugin<
- EndpointPluginStart,
EndpointPluginSetup,
- EndpointPluginStartDependencies,
- EndpointPluginSetupDependencies
+ EndpointPluginStart,
+ EndpointPluginSetupDependencies,
+ EndpointPluginStartDependencies
> {
- public setup(core: CoreSetup) {
+ public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) {
+ plugins.features.registerFeature({
+ id: 'endpoint',
+ name: 'Endpoint',
+ icon: 'bug',
+ navLinkId: 'endpoint',
+ app: ['endpoint', 'kibana'],
+ privileges: {
+ all: {
+ api: ['resolver'],
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: ['save'],
+ },
+ read: {
+ api: [],
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ },
+ });
const router = core.http.createRouter();
addRoutes(router);
}
public start() {}
+ public stop() {}
}
diff --git a/x-pack/plugins/endpoint/server/routes/index.ts b/x-pack/plugins/endpoint/server/routes/index.ts
index 517ee2a853660..8eab6cd384765 100644
--- a/x-pack/plugins/endpoint/server/routes/index.ts
+++ b/x-pack/plugins/endpoint/server/routes/index.ts
@@ -11,6 +11,9 @@ export function addRoutes(router: IRouter) {
{
path: '/api/endpoint/hello-world',
validate: false,
+ options: {
+ tags: ['access:resolver'],
+ },
},
async function greetingIndex(_context, _request, response) {
return response.ok({
diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts
index f613473dd87fb..34b2c5e324187 100644
--- a/x-pack/test/api_integration/apis/features/features/features.ts
+++ b/x-pack/test/api_integration/apis/features/features/features.ts
@@ -114,6 +114,7 @@ export default function({ getService }: FtrProviderContext) {
'maps',
'uptime',
'siem',
+ 'endpoint',
].sort()
);
});
diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts
index d4c8a3e68c50e..7b1984222404b 100644
--- a/x-pack/test/api_integration/apis/security/privileges.ts
+++ b/x-pack/test/api_integration/apis/security/privileges.ts
@@ -37,6 +37,7 @@ export default function({ getService }: FtrProviderContext) {
uptime: ['all', 'read'],
apm: ['all', 'read'],
siem: ['all', 'read'],
+ endpoint: ['all', 'read'],
},
global: ['all', 'read'],
space: ['all', 'read'],
diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts
new file mode 100644
index 0000000000000..6b3b423e293c2
--- /dev/null
+++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function({ getPageObjects, getService }: FtrProviderContext) {
+ const pageObjects = getPageObjects(['common']);
+ const spacesService = getService('spaces');
+ const testSubjects = getService('testSubjects');
+ const appsMenu = getService('appsMenu');
+
+ describe('spaces', () => {
+ describe('space with no features disabled', () => {
+ before(async () => {
+ await spacesService.create({
+ id: 'custom_space',
+ name: 'custom_space',
+ disabledFeatures: [],
+ });
+ });
+
+ after(async () => {
+ await spacesService.delete('custom_space');
+ });
+
+ it('shows endpoint navlink', async () => {
+ await pageObjects.common.navigateToApp('home', {
+ basePath: '/s/custom_space',
+ });
+ const navLinks = (await appsMenu.readLinks()).map(
+ (link: Record) => link.text
+ );
+ expect(navLinks).to.contain('EEndpoint');
+ });
+
+ it(`endpoint app shows 'Hello World'`, async () => {
+ await pageObjects.common.navigateToApp('endpoint', {
+ basePath: '/s/custom_space',
+ });
+ await testSubjects.existOrFail('welcomeTitle');
+ });
+ });
+
+ describe('space with endpoint disabled', () => {
+ before(async () => {
+ await spacesService.create({
+ id: 'custom_space',
+ name: 'custom_space',
+ disabledFeatures: ['endpoint'],
+ });
+ });
+
+ after(async () => {
+ await spacesService.delete('custom_space');
+ });
+
+ it(`doesn't show endpoint navlink`, async () => {
+ await pageObjects.common.navigateToApp('home', {
+ basePath: '/s/custom_space',
+ });
+ const navLinks = (await appsMenu.readLinks()).map(
+ (link: Record) => link.text
+ );
+ expect(navLinks).not.to.contain('EEndpoint');
+ });
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/index.ts b/x-pack/test/functional/apps/endpoint/feature_controls/index.ts
new file mode 100644
index 0000000000000..5f7e611fd966c
--- /dev/null
+++ b/x-pack/test/functional/apps/endpoint/feature_controls/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function({ loadTestFile }: FtrProviderContext) {
+ describe('feature controls', function() {
+ this.tags('skipFirefox');
+ loadTestFile(require.resolve('./endpoint_spaces'));
+ });
+}
diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts
new file mode 100644
index 0000000000000..1a0d3e973285b
--- /dev/null
+++ b/x-pack/test/functional/apps/endpoint/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function({ loadTestFile }: FtrProviderContext) {
+ describe('endpoint', function() {
+ this.tags('ciGroup7');
+
+ loadTestFile(require.resolve('./feature_controls'));
+ });
+}
diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js
index 8981c94b83578..17235c61c7d8c 100644
--- a/x-pack/test/functional/config.js
+++ b/x-pack/test/functional/config.js
@@ -56,6 +56,7 @@ export default async function({ readConfigFile }) {
resolve(__dirname, './apps/cross_cluster_replication'),
resolve(__dirname, './apps/remote_clusters'),
resolve(__dirname, './apps/transform'),
+ resolve(__dirname, './apps/endpoint'),
// This license_management file must be last because it is destructive.
resolve(__dirname, './apps/license_management'),
],
@@ -86,6 +87,7 @@ export default async function({ readConfigFile }) {
'--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"',
'--telemetry.banner=false',
'--timelion.ui.enabled=true',
+ '--xpack.endpoint.enabled=true',
],
},
uiSettings: {
@@ -197,6 +199,9 @@ export default async function({ readConfigFile }) {
pathname: '/app/kibana/',
hash: '/management/elasticsearch/transform',
},
+ endpoint: {
+ pathname: '/app/endpoint',
+ },
},
// choose where esArchiver should load archives from