mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Serverless] PLI features base architecture (#158179)
[Documentation](https://docs.google.com/document/d/1Ms8d8d_fbTTRHlBroEAKGNMNk3jFFgOAkVDRhqLxAPQ/edit?pli=1#) issue: https://github.com/elastic/kibana/issues/158810 ## Summary This PR is a cleanup to make [this POC](https://github.com/elastic/kibana/pull/155420) production ready - Serverless PLI features splitting in Security Solution, to allow/deny access to configured functionalities, using the current Kibana RBAC service. - Create the Upselling service to display Serveless-specific prompts in the application when features are not available - Create a `SecurityRoutePageWrapper` component that wraps Pages and displays the upsell when necessary. - We will refactor the code base to use `SecurityRoutePageWrapper` everywhere on another PR. - Create an Upsell page and section for entity analyticsbd8db822
-2f4b-4545-9da7-bedc07d93f90 ### test: Serverless: `yarn serverless-security`. * To change the product line you have to update `xpack.serverless.security.productLineIds` on `config/serverless.security.yml`. ESS: `yarn start` ### Glossary * PLI - Product Line Item (`Alert Triage`, `Osquery`, `Cases` , ... ) * Product Line - The product that the user is subscribed to (Security Essentials, Security Complete, ...) * essSecurity - New plugin with code that only runs for ESS offer (non-serverless). * App Feature - A security solution feature or group of features that can be disabled for a product line. It can be mapped to PLIs (`Alert Triage`, `Osquery`, `Cases` , ... ). * Capability - A string that when present represents that the user can access a given feature. A capability could be of the type UI or API (`read_cases`, `crud_cases`, ...). ### Current architecture  ### New architecture  ### How does it work? Every serverless product line (endpointEssentials, cloud essentials) can define which features are enabled:69d0fc15f4/x-pack/plugins/serverless_security/common/pli/pli_config.ts (L12-L19)
For ESS (non-serverless) offer we enable all features by default.69d0fc15f4/x-pack/plugins/ess_security/server/constants.ts (L10-L13)
A feature can define privileges:69d0fc15f4/x-pack/plugins/security_solution/server/lib/app_features/security_kibana_features.ts (L177-L185)
When the feature is enabled the privileges get merged into the base config and injected into kibana features.69d0fc15f4/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts (L61-L70)
### TODO - [x] lazy load these components - [x] Add unit test to: - ~SecurityRoutePageWrapper x-pack/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx~ - ~x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts~ - ~x-pack/plugins/security_solution/public/common/lib/capabilities/has_capabilities.ts~ - ~x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts~ - ~x-pack/plugins/serverless_security/common/pli/pli_features.ts~ - ~x-pack/plugins/serverless_security/public/components/upselling/register_upsellings.tsx~ - ~x-pack/plugins/security_solution/server/lib/app_features/app_features.ts~ ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
248a1346af
commit
88aa68aec8
86 changed files with 3019 additions and 909 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -342,6 +342,7 @@ packages/kbn-eslint-plugin-eslint @elastic/kibana-operations
|
|||
packages/kbn-eslint-plugin-imports @elastic/kibana-operations
|
||||
packages/kbn-eslint-plugin-telemetry @elastic/actionable-observability
|
||||
x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security
|
||||
x-pack/plugins/ess_security @elastic/security-solution
|
||||
src/plugins/event_annotation @elastic/kibana-visualizations
|
||||
x-pack/test/plugin_api_integration/plugins/event_log @elastic/response-ops
|
||||
x-pack/plugins/event_log @elastic/response-ops
|
||||
|
|
|
@ -8,6 +8,7 @@ xpack.uptime.enabled: false
|
|||
|
||||
## Enable the Serverless Security plugin
|
||||
xpack.serverless.security.enabled: true
|
||||
xpack.serverless.security.productLineIds: ['securityComplete']
|
||||
|
||||
## Set the home route
|
||||
uiSettings.overrides.defaultRoute: /app/security/get_started
|
||||
|
|
|
@ -4,6 +4,9 @@ xpack.serverless.plugin.enabled: true
|
|||
xpack.fleet.internal.fleetServerStandalone: true
|
||||
xpack.fleet.internal.disableILMPolicies: true
|
||||
|
||||
# Ess plugins
|
||||
xpack.ess.security.enabled: false
|
||||
|
||||
# Management team plugins
|
||||
xpack.upgrade_assistant.enabled: false
|
||||
xpack.rollup.enabled: false
|
||||
|
|
|
@ -534,6 +534,10 @@ security and spaces filtering.
|
|||
|This plugin provides Kibana user interfaces for managing the Enterprise Search solution and its products, App Search and Workplace Search.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/ess_security/README.md[essSecurity]
|
||||
|This plugin contains the ESS/on-prem deployments (non-serverless) customizations for Security Solution.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog]
|
||||
|The event log plugin provides a persistent history of alerting and action
|
||||
activities.
|
||||
|
|
|
@ -375,6 +375,7 @@
|
|||
"@kbn/es-types": "link:packages/kbn-es-types",
|
||||
"@kbn/es-ui-shared-plugin": "link:src/plugins/es_ui_shared",
|
||||
"@kbn/eso-plugin": "link:x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin",
|
||||
"@kbn/ess-security": "link:x-pack/plugins/ess_security",
|
||||
"@kbn/event-annotation-plugin": "link:src/plugins/event_annotation",
|
||||
"@kbn/event-log-fixture-plugin": "link:x-pack/test/plugin_api_integration/plugins/event_log",
|
||||
"@kbn/event-log-plugin": "link:x-pack/plugins/event_log",
|
||||
|
|
|
@ -38,6 +38,7 @@ pageLoadAssetSize:
|
|||
embeddable: 87309
|
||||
embeddableEnhanced: 22107
|
||||
enterpriseSearch: 35741
|
||||
essSecurity: 16573
|
||||
esUiShared: 326654
|
||||
eventAnnotation: 22000
|
||||
exploratoryView: 74673
|
||||
|
|
|
@ -678,6 +678,8 @@
|
|||
"@kbn/eslint-plugin-telemetry/*": ["packages/kbn-eslint-plugin-telemetry/*"],
|
||||
"@kbn/eso-plugin": ["x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin"],
|
||||
"@kbn/eso-plugin/*": ["x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/*"],
|
||||
"@kbn/ess-security": ["x-pack/plugins/ess_security"],
|
||||
"@kbn/ess-security/*": ["x-pack/plugins/ess_security/*"],
|
||||
"@kbn/event-annotation-plugin": ["src/plugins/event_annotation"],
|
||||
"@kbn/event-annotation-plugin/*": ["src/plugins/event_annotation/*"],
|
||||
"@kbn/event-log-fixture-plugin": ["x-pack/test/plugin_api_integration/plugins/event_log"],
|
||||
|
|
2
x-pack/plugins/ess_security/.gitignore
vendored
Normal file
2
x-pack/plugins/ess_security/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/build
|
||||
/target
|
3
x-pack/plugins/ess_security/README.md
Executable file
3
x-pack/plugins/ess_security/README.md
Executable file
|
@ -0,0 +1,3 @@
|
|||
# essSecurity
|
||||
|
||||
This plugin contains the ESS/on-prem deployments (non-serverless) customizations for Security Solution.
|
9
x-pack/plugins/ess_security/common/index.ts
Normal file
9
x-pack/plugins/ess_security/common/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const PLUGIN_ID = 'essSecurity';
|
||||
export const PLUGIN_NAME = 'essSecurity';
|
17
x-pack/plugins/ess_security/kibana.jsonc
Normal file
17
x-pack/plugins/ess_security/kibana.jsonc
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/ess-security",
|
||||
"owner": "@elastic/security-solution",
|
||||
"description": "ESS customizations for Security Solution.",
|
||||
"plugin": {
|
||||
"id": "essSecurity",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"configPath": ["xpack", "ess", "security"],
|
||||
"requiredPlugins": [
|
||||
"securitySolution",
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": []
|
||||
}
|
||||
}
|
11
x-pack/plugins/ess_security/package.json
Normal file
11
x-pack/plugins/ess_security/package.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@kbn/ess-security",
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "yarn plugin-helpers build",
|
||||
"plugin-helpers": "node ../../../scripts/plugin_helpers",
|
||||
"kbn": "node ../../../scripts/kbn"
|
||||
}
|
||||
}
|
17
x-pack/plugins/ess_security/public/index.ts
Normal file
17
x-pack/plugins/ess_security/public/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { EssSecurityPlugin } from './plugin';
|
||||
|
||||
// This exports static code and TypeScript types,
|
||||
// as well as, Kibana Platform `plugin()` initializer.
|
||||
export function plugin(_initializerContext: PluginInitializerContext) {
|
||||
return new EssSecurityPlugin();
|
||||
}
|
||||
|
||||
export type { EssSecurityPluginSetup, EssSecurityPluginStart } from './types';
|
42
x-pack/plugins/ess_security/public/plugin.ts
Normal file
42
x-pack/plugins/ess_security/public/plugin.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import {
|
||||
EssSecurityPluginSetup,
|
||||
EssSecurityPluginStart,
|
||||
EssSecurityPluginSetupDependencies,
|
||||
EssSecurityPluginStartDependencies,
|
||||
} from './types';
|
||||
|
||||
export class EssSecurityPlugin
|
||||
implements
|
||||
Plugin<
|
||||
EssSecurityPluginSetup,
|
||||
EssSecurityPluginStart,
|
||||
EssSecurityPluginSetupDependencies,
|
||||
EssSecurityPluginStartDependencies
|
||||
>
|
||||
{
|
||||
constructor() {}
|
||||
|
||||
public setup(
|
||||
_core: CoreSetup,
|
||||
_setupDeps: EssSecurityPluginSetupDependencies
|
||||
): EssSecurityPluginSetup {
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(
|
||||
_core: CoreStart,
|
||||
_startDeps: EssSecurityPluginStartDependencies
|
||||
): EssSecurityPluginStart {
|
||||
return {};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
25
x-pack/plugins/ess_security/public/types.ts
Normal file
25
x-pack/plugins/ess_security/public/types.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 type {
|
||||
PluginSetup as SecuritySolutionPluginSetup,
|
||||
PluginStart as SecuritySolutionPluginStart,
|
||||
} from '@kbn/security-solution-plugin/public';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface EssSecurityPluginSetup {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface EssSecurityPluginStart {}
|
||||
|
||||
export interface EssSecurityPluginSetupDependencies {
|
||||
securitySolution: SecuritySolutionPluginSetup;
|
||||
}
|
||||
|
||||
export interface EssSecurityPluginStartDependencies {
|
||||
securitySolution: SecuritySolutionPluginStart;
|
||||
}
|
18
x-pack/plugins/ess_security/server/config.ts
Normal file
18
x-pack/plugins/ess_security/server/config.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
});
|
||||
export type EssSecurityConfig = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<EssSecurityConfig> = {
|
||||
schema: configSchema,
|
||||
};
|
13
x-pack/plugins/ess_security/server/constants.ts
Normal file
13
x-pack/plugins/ess_security/server/constants.ts
Normal file
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AppFeatureKey, AppFeatureKeys } from '@kbn/security-solution-plugin/common';
|
||||
|
||||
export const DEFAULT_APP_FEATURES: AppFeatureKeys = {
|
||||
[AppFeatureKey.advancedInsights]: true,
|
||||
[AppFeatureKey.casesConnectors]: true,
|
||||
};
|
19
x-pack/plugins/ess_security/server/index.ts
Normal file
19
x-pack/plugins/ess_security/server/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from '@kbn/core/server';
|
||||
|
||||
import { EssSecurityPlugin } from './plugin';
|
||||
export { config } from './config';
|
||||
|
||||
// This exports static code and TypeScript types,
|
||||
// as well as, Kibana Platform `plugin()` initializer.
|
||||
export function plugin(_initializerContext: PluginInitializerContext) {
|
||||
return new EssSecurityPlugin();
|
||||
}
|
||||
|
||||
export type { EssSecurityPluginSetup, EssSecurityPluginStart } from './types';
|
39
x-pack/plugins/ess_security/server/plugin.ts
Normal file
39
x-pack/plugins/ess_security/server/plugin.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { Plugin, CoreSetup } from '@kbn/core/server';
|
||||
import { DEFAULT_APP_FEATURES } from './constants';
|
||||
|
||||
import {
|
||||
EssSecurityPluginSetup,
|
||||
EssSecurityPluginStart,
|
||||
EssSecurityPluginSetupDependencies,
|
||||
EssSecurityPluginStartDependencies,
|
||||
} from './types';
|
||||
|
||||
export class EssSecurityPlugin
|
||||
implements
|
||||
Plugin<
|
||||
EssSecurityPluginSetup,
|
||||
EssSecurityPluginStart,
|
||||
EssSecurityPluginSetupDependencies,
|
||||
EssSecurityPluginStartDependencies
|
||||
>
|
||||
{
|
||||
constructor() {}
|
||||
|
||||
public setup(_coreSetup: CoreSetup, pluginsSetup: EssSecurityPluginSetupDependencies) {
|
||||
pluginsSetup.securitySolution.setAppFeatures(DEFAULT_APP_FEATURES);
|
||||
return {};
|
||||
}
|
||||
|
||||
public start() {
|
||||
return {};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
24
x-pack/plugins/ess_security/server/types.ts
Normal file
24
x-pack/plugins/ess_security/server/types.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 {
|
||||
PluginSetup as SecuritySolutionPluginSetup,
|
||||
PluginStart as SecuritySolutionPluginStart,
|
||||
} from '@kbn/security-solution-plugin/server';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface EssSecurityPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface EssSecurityPluginStart {}
|
||||
|
||||
export interface EssSecurityPluginSetupDependencies {
|
||||
securitySolution: SecuritySolutionPluginSetup;
|
||||
}
|
||||
|
||||
export interface EssSecurityPluginStartDependencies {
|
||||
securitySolution: SecuritySolutionPluginStart;
|
||||
}
|
22
x-pack/plugins/ess_security/tsconfig.json
Normal file
22
x-pack/plugins/ess_security/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"common/**/*.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/security-solution-plugin",
|
||||
]
|
||||
}
|
|
@ -8,8 +8,11 @@
|
|||
// TODO(jbudz): should be removed when upgrading to TS@4.8
|
||||
// this is a skip for the errors created when typechecking with isolatedModules
|
||||
export {};
|
||||
export { APP_UI_ID, SecurityPageName } from './constants';
|
||||
export { APP_UI_ID, APP_ID, CASES_FEATURE_ID, SERVER_APP_ID, SecurityPageName } from './constants';
|
||||
export { ELASTIC_SECURITY_RULE_ID } from './detection_engine/constants';
|
||||
export { allowedExperimentalValues, type ExperimentalFeatures } from './experimental_features';
|
||||
export type { AppFeatureKeys } from './types/app_features';
|
||||
export { AppFeatureKey } from './types/app_features';
|
||||
|
||||
// Careful of exporting anything from this file as any file(s) you export here will cause your page bundle size to increase.
|
||||
// If you're using functions/types/etc... internally it's best to import directly from their paths than expose the functions/types/etc... here.
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum AppFeatureSecurityKey {
|
||||
/**
|
||||
* Enables Advanced Insights (Entity Risk, GenAI)
|
||||
*/
|
||||
advancedInsights = 'advanced_insights',
|
||||
}
|
||||
|
||||
export enum AppFeatureCasesKey {
|
||||
/**
|
||||
* Enables Cases Connectors
|
||||
*/
|
||||
casesConnectors = 'cases_connectors',
|
||||
}
|
||||
|
||||
// Merges the two enums.
|
||||
// We need to merge the value and the type and export both to replicate how enum works.
|
||||
export const AppFeatureKey = { ...AppFeatureSecurityKey, ...AppFeatureCasesKey };
|
||||
export type AppFeatureKey = AppFeatureSecurityKey | AppFeatureCasesKey;
|
||||
|
||||
type AppFeatureSecurityKeys = { [key in AppFeatureSecurityKey]: boolean };
|
||||
type AppFeatureCasesKeys = { [key in AppFeatureCasesKey]: boolean };
|
||||
export type AppFeatureKeys = AppFeatureSecurityKeys & AppFeatureCasesKeys;
|
|
@ -14,6 +14,26 @@ export interface DescriptionList {
|
|||
description: NonNullable<ReactNode>;
|
||||
}
|
||||
|
||||
// Recursive partial object type. inspired by EUI
|
||||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends NonAny[]
|
||||
? T[P]
|
||||
: T[P] extends readonly NonAny[]
|
||||
? T[P]
|
||||
: T[P] extends Array<infer U>
|
||||
? Array<RecursivePartial<U>>
|
||||
: T[P] extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<RecursivePartial<U>>
|
||||
: T[P] extends Set<infer V>
|
||||
? Set<RecursivePartial<V>>
|
||||
: T[P] extends Map<infer K, infer V>
|
||||
? Map<K, RecursivePartial<V>>
|
||||
: T[P] extends NonAny
|
||||
? T[P]
|
||||
: RecursivePartial<T[P]>;
|
||||
};
|
||||
type NonAny = number | boolean | string | symbol | null;
|
||||
|
||||
export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) =>
|
||||
runtimeTypes.union([type, runtimeTypes.null]);
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import { updateAppLinks } from '../../../links';
|
|||
import { allowedExperimentalValues } from '../../../../../common/experimental_features';
|
||||
import { AlertDetailRouteType } from '../../../../detections/pages/alert_details/types';
|
||||
import { UsersTableType } from '../../../../explore/users/store/model';
|
||||
import { UpsellingService } from '../../../lib/upsellings';
|
||||
|
||||
const mockUseRouteSpy = jest.fn();
|
||||
jest.mock('../../../utils/route/use_route_spy', () => ({
|
||||
|
@ -171,6 +172,7 @@ describe('Navigation Breadcrumbs', () => {
|
|||
crud: true,
|
||||
},
|
||||
},
|
||||
upselling: new UpsellingService(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { AppLinkItems } from '../../links';
|
|||
import { updateAppLinks } from '../../links';
|
||||
import { mockGlobalState } from '../../mock';
|
||||
import type { Capabilities } from '@kbn/core-capabilities-common';
|
||||
import { UpsellingService } from '../../lib/upsellings';
|
||||
|
||||
const defaultAppLinks: AppLinkItems = [
|
||||
{
|
||||
|
@ -27,11 +28,14 @@ const defaultAppLinks: AppLinkItems = [
|
|||
},
|
||||
];
|
||||
|
||||
const mockUpselling = new UpsellingService();
|
||||
|
||||
describe('helpers', () => {
|
||||
beforeAll(() => {
|
||||
updateAppLinks(defaultAppLinks, {
|
||||
capabilities: {} as unknown as Capabilities,
|
||||
experimentalFeatures: mockGlobalState.app.enableExperimental,
|
||||
upselling: mockUpselling,
|
||||
});
|
||||
});
|
||||
it('returns the search string', () => {
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { SecurityRoutePageWrapper } from '.';
|
||||
import { SecurityPageName } from '../../../../common';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { generateHistoryMock } from '../../utils/route/mocks';
|
||||
|
||||
const mockUseLinkAuthorized = jest.fn();
|
||||
const mockUseUpsellingPage = jest.fn();
|
||||
|
||||
jest.mock('../../links', () => ({
|
||||
useLinkAuthorized: () => mockUseLinkAuthorized(),
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/use_upselling', () => ({
|
||||
useUpsellingPage: () => mockUseUpsellingPage(),
|
||||
}));
|
||||
|
||||
const TEST_COMPONENT_SUBJ = 'test-component';
|
||||
const TestComponent = () => <div data-test-subj={TEST_COMPONENT_SUBJ} />;
|
||||
const mockHistory = generateHistoryMock();
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={mockHistory}>
|
||||
<TestProviders>{children}</TestProviders>
|
||||
</Router>
|
||||
);
|
||||
|
||||
describe('SecurityRoutePageWrapper', () => {
|
||||
it('renders children when authorized', () => {
|
||||
mockUseLinkAuthorized.mockReturnValue(true);
|
||||
const { getByTestId } = render(
|
||||
<Router history={mockHistory}>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>
|
||||
</Router>,
|
||||
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders UpsellPage when unauthorized and UpsellPage is available', () => {
|
||||
const TestUpsellPage = () => <div data-test-subj={'test-upsell-page'} />;
|
||||
|
||||
mockUseLinkAuthorized.mockReturnValue(false);
|
||||
mockUseUpsellingPage.mockReturnValue(TestUpsellPage);
|
||||
const { getByTestId } = render(
|
||||
<Router history={mockHistory}>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>
|
||||
</Router>,
|
||||
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId('test-upsell-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders NoPrivilegesPage when unauthorized and UpsellPage is unavailable', () => {
|
||||
mockUseLinkAuthorized.mockReturnValue(false);
|
||||
mockUseUpsellingPage.mockReturnValue(undefined);
|
||||
const { getByTestId } = render(
|
||||
<Router history={mockHistory}>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>
|
||||
</Router>,
|
||||
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId('noPrivilegesPage')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Write unit test for file /Users/pablo.nevesmachado/workspace/kibana/x-pack/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import type { SecurityPageName } from '../../../../common';
|
||||
import { useLinkAuthorized } from '../../links';
|
||||
import { NoPrivilegesPage } from '../no_privileges';
|
||||
import { useUpsellingPage } from '../../hooks/use_upselling';
|
||||
import { SpyRoute } from '../../utils/route/spy_routes';
|
||||
|
||||
interface SecurityRoutePageWrapperProps {
|
||||
pageName: SecurityPageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is created to wrap all the pages in the security solution app.
|
||||
*
|
||||
* It handles application tracking and upselling.
|
||||
*
|
||||
* When using this component make sure it render bellow `SecurityPageWrapper` and
|
||||
* that you removed the `TrackApplicationView` component.
|
||||
*
|
||||
* Ex:
|
||||
* ```
|
||||
* <PluginTemplateWrapper>
|
||||
* <SecurityRoutePageWrapper pageName={SecurityPageName.myPage}>
|
||||
* <MyPage />
|
||||
* </SecurityRoutePageWrapper>
|
||||
* </PluginTemplateWrapper>
|
||||
* ```
|
||||
*/
|
||||
export const SecurityRoutePageWrapper: React.FC<SecurityRoutePageWrapperProps> = ({
|
||||
children,
|
||||
pageName,
|
||||
}) => {
|
||||
const isAuthorized = useLinkAuthorized(pageName);
|
||||
const UpsellPage = useUpsellingPage(pageName);
|
||||
|
||||
if (isAuthorized) {
|
||||
return <TrackApplicationView viewId={pageName}>{children}</TrackApplicationView>;
|
||||
}
|
||||
|
||||
if (UpsellPage) {
|
||||
return (
|
||||
<>
|
||||
<SpyRoute pageName={pageName} />
|
||||
<UpsellPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpyRoute pageName={pageName} />
|
||||
<NoPrivilegesPage
|
||||
pageName={pageName}
|
||||
docLinkSelector={(docLinks) => docLinks.siem.privileges}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import { SecurityPageName } from '../../../common';
|
||||
import { UpsellingService } from '../lib/upsellings';
|
||||
import { useUpsellingComponent, useUpsellingPage } from './use_upselling';
|
||||
|
||||
const mockUpselling = new UpsellingService();
|
||||
|
||||
jest.mock('../lib/kibana', () => {
|
||||
const original = jest.requireActual('../lib/kibana');
|
||||
return {
|
||||
...original,
|
||||
useKibana: () => ({
|
||||
...original.useKibana(),
|
||||
services: {
|
||||
...original.useKibana().services,
|
||||
upselling: mockUpselling,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const TestComponent = () => <div>{'TEST 1 2 3'}</div>;
|
||||
|
||||
describe('use_upselling', () => {
|
||||
test('useUpsellingComponent returns sections', () => {
|
||||
mockUpselling.registerSections({
|
||||
entity_analytics_panel: TestComponent,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpsellingComponent('entity_analytics_panel'));
|
||||
expect(result.current).toBe(TestComponent);
|
||||
});
|
||||
|
||||
test('useUpsellingPage returns pages', () => {
|
||||
mockUpselling.registerPages({
|
||||
[SecurityPageName.hosts]: TestComponent,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpsellingPage(SecurityPageName.hosts));
|
||||
expect(result.current).toBe(TestComponent);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { UpsellingSectionId } from '../lib/upsellings';
|
||||
import { useKibana } from '../lib/kibana';
|
||||
import type { SecurityPageName } from '../../../common';
|
||||
|
||||
export const useUpsellingComponent = (id: UpsellingSectionId): React.ComponentType | null => {
|
||||
const { upselling } = useKibana().services;
|
||||
const upsellingSections = useObservable(upselling.sections$);
|
||||
|
||||
return useMemo(() => upsellingSections?.get(id) ?? null, [id, upsellingSections]);
|
||||
};
|
||||
|
||||
export const useUpsellingPage = (pageName: SecurityPageName): React.ComponentType | null => {
|
||||
const { upselling } = useKibana().services;
|
||||
const UpsellingPage = useMemo(() => upselling.getPageUpselling(pageName), [pageName, upselling]);
|
||||
|
||||
return UpsellingPage ?? null;
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { hasCapabilities } from './has_capabilities';
|
||||
|
||||
const EMPTY_CAPABILITIES = { navLinks: {}, management: {}, catalogue: {} };
|
||||
const SAMPLE_CAPABILITY = { show: true, crud: true };
|
||||
|
||||
describe('hasCapabilities', () => {
|
||||
it('returns true when no capabilities are required', () => {
|
||||
expect(hasCapabilities(EMPTY_CAPABILITIES)).toEqual(true);
|
||||
});
|
||||
|
||||
describe('when requiredCapabilities is a string', () => {
|
||||
it('returns false when the capability is not present', () => {
|
||||
expect(hasCapabilities(EMPTY_CAPABILITIES, 'missingCapability')).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when the capability is present', () => {
|
||||
const capabilities = {
|
||||
...EMPTY_CAPABILITIES,
|
||||
requiredCapability: SAMPLE_CAPABILITY,
|
||||
};
|
||||
expect(hasCapabilities(capabilities, 'requiredCapability')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when requiredCapabilities is an array', () => {
|
||||
describe('when there is only one array (OR)', () => {
|
||||
it('returns false when none of the capabilities are present', () => {
|
||||
expect(
|
||||
hasCapabilities(EMPTY_CAPABILITIES, ['missingCapability1', 'missingCapability2'])
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when any of the capabilities are present', () => {
|
||||
const capabilities = {
|
||||
...EMPTY_CAPABILITIES,
|
||||
requiredCapability: SAMPLE_CAPABILITY,
|
||||
};
|
||||
expect(hasCapabilities(capabilities, ['requiredCapability', 'missingCapability'])).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there subArrays (And)', () => {
|
||||
it('returns false when one of the capabilities is not present', () => {
|
||||
const capabilities = {
|
||||
...EMPTY_CAPABILITIES,
|
||||
requiredCapability1: SAMPLE_CAPABILITY,
|
||||
};
|
||||
|
||||
expect(
|
||||
hasCapabilities(capabilities, [['requiredCapability1', 'requiredCapability2']])
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when both capabilities are present', () => {
|
||||
const capabilities = {
|
||||
...EMPTY_CAPABILITIES,
|
||||
requiredCapability1: SAMPLE_CAPABILITY,
|
||||
requiredCapability2: SAMPLE_CAPABILITY,
|
||||
};
|
||||
expect(
|
||||
hasCapabilities(capabilities, [['requiredCapability1', 'requiredCapability2']])
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { get, isArray } from 'lodash';
|
||||
import type { Capabilities } from '@kbn/core/public';
|
||||
|
||||
/**
|
||||
* The format of defining features supports OR and AND mechanism. To specify features in an OR fashion
|
||||
* they can be defined in a single level array like: [requiredFeature1, requiredFeature2]. If either of these features
|
||||
* is satisfied the link would be included. To require that the features be AND'd together a second level array
|
||||
* can be specified: [feature1, [feature2, feature3]] this would result in feature1 || (feature2 && feature3). To specify
|
||||
* features that all must be and'd together an example would be: [[feature1, feature2]], this would result in the boolean
|
||||
* operation feature1 && feature2.
|
||||
*
|
||||
* The final format is to specify a single feature, this would be like: features: feature1, which is the same as
|
||||
* features: [feature1]
|
||||
*/
|
||||
export type RequiredCapabilities = string | Array<string | string[]>;
|
||||
|
||||
export const hasCapabilities = (
|
||||
capabilities: Capabilities,
|
||||
requiredCapabilities?: RequiredCapabilities
|
||||
): boolean => {
|
||||
if (!requiredCapabilities) {
|
||||
return true;
|
||||
}
|
||||
if (!isArray(requiredCapabilities)) {
|
||||
return !!get(capabilities, requiredCapabilities, false);
|
||||
} else {
|
||||
return requiredCapabilities.some((linkCapabilityKeyOr) => {
|
||||
if (isArray(linkCapabilityKeyOr)) {
|
||||
return linkCapabilityKeyOr.every((linkCapabilityKeyAnd) =>
|
||||
get(capabilities, linkCapabilityKeyAnd, false)
|
||||
);
|
||||
}
|
||||
return get(capabilities, linkCapabilityKeyOr, false);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { hasCapabilities, type RequiredCapabilities } from './has_capabilities';
|
|
@ -47,6 +47,7 @@ import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks
|
|||
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { of } from 'rxjs';
|
||||
import { UpsellingService } from '../upsellings';
|
||||
|
||||
const mockUiSettings: Record<string, unknown> = {
|
||||
[DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' },
|
||||
|
@ -191,6 +192,7 @@ export const createStartServicesMock = (
|
|||
cloudExperiments,
|
||||
guidedOnboarding,
|
||||
isSidebarEnabled$: of(true),
|
||||
upselling: new UpsellingService(),
|
||||
} as unknown as StartServices;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { UpsellingService } from './upselling_service';
|
||||
export type { PageUpsellings, SectionUpsellings, UpsellingSectionId } from './types';
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SecurityPageName } from '../../../../common';
|
||||
|
||||
export type PageUpsellings = Partial<Record<SecurityPageName, React.ComponentType>>;
|
||||
export type SectionUpsellings = Partial<Record<UpsellingSectionId, React.ComponentType>>;
|
||||
|
||||
export type UpsellingSectionId = 'entity_analytics_panel';
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { SecurityPageName } from '../../../../common';
|
||||
import { UpsellingService } from './upselling_service';
|
||||
|
||||
const TestComponent = () => <div>{'TEST component'}</div>;
|
||||
|
||||
describe('UpsellingService', () => {
|
||||
it('registers sections', async () => {
|
||||
const service = new UpsellingService();
|
||||
service.registerSections({
|
||||
entity_analytics_panel: TestComponent,
|
||||
});
|
||||
|
||||
const value = await firstValueFrom(service.sections$);
|
||||
|
||||
expect(value.get('entity_analytics_panel')).toEqual(TestComponent);
|
||||
});
|
||||
|
||||
it('registers pages', async () => {
|
||||
const service = new UpsellingService();
|
||||
service.registerPages({
|
||||
[SecurityPageName.hosts]: TestComponent,
|
||||
});
|
||||
|
||||
const value = await firstValueFrom(service.pages$);
|
||||
|
||||
expect(value.get(SecurityPageName.hosts)).toEqual(TestComponent);
|
||||
});
|
||||
|
||||
it('"isPageUpsellable" returns true when page is upsellable', () => {
|
||||
const service = new UpsellingService();
|
||||
service.registerPages({
|
||||
[SecurityPageName.hosts]: TestComponent,
|
||||
});
|
||||
|
||||
expect(service.isPageUpsellable(SecurityPageName.hosts)).toEqual(true);
|
||||
});
|
||||
|
||||
it('"getPageUpselling" returns page component when page is upsellable', () => {
|
||||
const service = new UpsellingService();
|
||||
service.registerPages({
|
||||
[SecurityPageName.hosts]: TestComponent,
|
||||
});
|
||||
|
||||
expect(service.getPageUpselling(SecurityPageName.hosts)).toEqual(TestComponent);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 type { Observable } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { SecurityPageName } from '../../../../common';
|
||||
import type { SectionUpsellings, PageUpsellings, UpsellingSectionId } from './types';
|
||||
|
||||
export class UpsellingService {
|
||||
private sections: Map<UpsellingSectionId, React.ComponentType>;
|
||||
private pages: Map<SecurityPageName, React.ComponentType>;
|
||||
private sectionsSubject$: BehaviorSubject<Map<UpsellingSectionId, React.ComponentType>>;
|
||||
private pagesSubject$: BehaviorSubject<Map<SecurityPageName, React.ComponentType>>;
|
||||
|
||||
public sections$: Observable<Map<UpsellingSectionId, React.ComponentType>>;
|
||||
public pages$: Observable<Map<SecurityPageName, React.ComponentType>>;
|
||||
|
||||
constructor() {
|
||||
this.sections = new Map();
|
||||
this.sectionsSubject$ = new BehaviorSubject(new Map());
|
||||
this.sections$ = this.sectionsSubject$.asObservable();
|
||||
this.pages = new Map();
|
||||
this.pagesSubject$ = new BehaviorSubject(new Map());
|
||||
this.pages$ = this.pagesSubject$.asObservable();
|
||||
}
|
||||
|
||||
registerSections(sections: SectionUpsellings) {
|
||||
Object.entries(sections).forEach(([sectionId, component]) => {
|
||||
this.sections.set(sectionId as UpsellingSectionId, component);
|
||||
});
|
||||
this.sectionsSubject$.next(this.sections);
|
||||
}
|
||||
|
||||
registerPages(pages: PageUpsellings) {
|
||||
Object.entries(pages).forEach(([pageId, component]) => {
|
||||
this.pages.set(pageId as SecurityPageName, component);
|
||||
});
|
||||
this.pagesSubject$.next(this.pages);
|
||||
}
|
||||
|
||||
isPageUpsellable(id: SecurityPageName) {
|
||||
return this.pages.has(id);
|
||||
}
|
||||
|
||||
getPageUpselling(id: SecurityPageName) {
|
||||
return this.pages.get(id);
|
||||
}
|
||||
}
|
|
@ -18,9 +18,11 @@ import {
|
|||
needsUrlState,
|
||||
updateAppLinks,
|
||||
useLinkExists,
|
||||
hasCapabilities,
|
||||
} from './links';
|
||||
import { createCapabilities } from './test_utils';
|
||||
import { hasCapabilities } from '../lib/capabilities';
|
||||
import { UpsellingService } from '../lib/upsellings';
|
||||
import React from 'react';
|
||||
|
||||
const defaultAppLinks: AppLinkItems = [
|
||||
{
|
||||
|
@ -43,6 +45,8 @@ const defaultAppLinks: AppLinkItems = [
|
|||
},
|
||||
];
|
||||
|
||||
const mockUpselling = new UpsellingService();
|
||||
|
||||
const mockExperimentalDefaults = mockGlobalState.app.enableExperimental;
|
||||
|
||||
const mockCapabilities = {
|
||||
|
@ -90,6 +94,7 @@ describe('Security links', () => {
|
|||
capabilities: mockCapabilities,
|
||||
experimentalFeatures: mockExperimentalDefaults,
|
||||
license: mockLicense,
|
||||
upselling: mockUpselling,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -168,6 +173,7 @@ describe('Security links', () => {
|
|||
flagDisabled: false,
|
||||
} as unknown as typeof mockExperimentalDefaults,
|
||||
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
|
||||
upselling: mockUpselling,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
@ -175,6 +181,74 @@ describe('Security links', () => {
|
|||
|
||||
expect(result.current).toStrictEqual([networkLinkItem]);
|
||||
});
|
||||
|
||||
it('should return unauthorized page when page has upselling', async () => {
|
||||
const upselling = new UpsellingService();
|
||||
upselling.registerPages({ [SecurityPageName.network]: () => <span /> });
|
||||
|
||||
const { result, waitForNextUpdate } = renderUseAppLinks();
|
||||
const networkLinkItem = {
|
||||
id: SecurityPageName.network,
|
||||
title: 'Network',
|
||||
path: '/network',
|
||||
capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${CASES_FEATURE_ID}.write_cases`],
|
||||
experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults,
|
||||
hideWhenExperimentalKey: 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults,
|
||||
licenseType: 'basic' as const,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
updateAppLinks(
|
||||
[
|
||||
{
|
||||
...networkLinkItem,
|
||||
// The following links should be filtered out because network link is unauthorized
|
||||
links: [
|
||||
{
|
||||
id: SecurityPageName.networkDns,
|
||||
title: 'dns',
|
||||
path: '/dns',
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.networkHttp,
|
||||
title: 'Http',
|
||||
path: '/http',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// should be excluded by license with all its links
|
||||
id: SecurityPageName.hosts,
|
||||
title: 'Hosts',
|
||||
path: '/hosts',
|
||||
licenseType: 'platinum',
|
||||
links: [
|
||||
{
|
||||
id: SecurityPageName.hostsEvents,
|
||||
title: 'Events',
|
||||
path: '/events',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
capabilities: {
|
||||
...mockCapabilities,
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
},
|
||||
experimentalFeatures: {
|
||||
flagEnabled: true,
|
||||
flagDisabled: false,
|
||||
} as unknown as typeof mockExperimentalDefaults,
|
||||
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
|
||||
upselling,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual([{ ...networkLinkItem, unauthorized: true }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLinkExists', () => {
|
||||
|
@ -204,6 +278,7 @@ describe('Security links', () => {
|
|||
capabilities: mockCapabilities,
|
||||
experimentalFeatures: mockExperimentalDefaults,
|
||||
license: mockLicense,
|
||||
upselling: new UpsellingService(),
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
@ -234,6 +309,7 @@ describe('Security links', () => {
|
|||
capabilities: mockCapabilities,
|
||||
experimentalFeatures: mockExperimentalDefaults,
|
||||
license: mockLicense,
|
||||
upselling: mockUpselling,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
@ -291,33 +367,33 @@ describe('Security links', () => {
|
|||
const pushCases = 'securitySolutionCases.push_cases';
|
||||
|
||||
it('returns false when capabilities is an empty array', () => {
|
||||
expect(hasCapabilities([], createCapabilities())).toBeFalsy();
|
||||
expect(hasCapabilities(createCapabilities(), [])).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true when the capability requested is specified as a single value', () => {
|
||||
expect(hasCapabilities(siemShow, createCapabilities({ siem: { show: true } }))).toBeTruthy();
|
||||
expect(hasCapabilities(createCapabilities({ siem: { show: true } }), siemShow)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when the capability requested is a single entry in an array', () => {
|
||||
expect(
|
||||
hasCapabilities([siemShow], createCapabilities({ siem: { show: true } }))
|
||||
hasCapabilities(createCapabilities({ siem: { show: true } }), [siemShow])
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns true when the capability requested is a single entry in an AND'd array format", () => {
|
||||
expect(
|
||||
hasCapabilities([[siemShow]], createCapabilities({ siem: { show: true } }))
|
||||
hasCapabilities(createCapabilities({ siem: { show: true } }), [[siemShow]])
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when only one requested capability is found in an OR situation', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[siemShow, createCases],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { create_cases: false },
|
||||
})
|
||||
}),
|
||||
[siemShow, createCases]
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -325,11 +401,11 @@ describe('Security links', () => {
|
|||
it('returns true when only the create_cases requested capability is found in an OR situation', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[siemShow, createCases],
|
||||
createCapabilities({
|
||||
siem: { show: false },
|
||||
securitySolutionCases: { create_cases: true },
|
||||
})
|
||||
}),
|
||||
[siemShow, createCases]
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -337,11 +413,11 @@ describe('Security links', () => {
|
|||
it('returns false when none of the requested capabilities are found in an OR situation', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[readCases, createCases],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { create_cases: false },
|
||||
})
|
||||
}),
|
||||
[readCases, createCases]
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
@ -349,11 +425,11 @@ describe('Security links', () => {
|
|||
it('returns true when all of the requested capabilities are found in an AND situation', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[[readCases, createCases]],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: true, create_cases: true },
|
||||
})
|
||||
}),
|
||||
[[readCases, createCases]]
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -361,11 +437,11 @@ describe('Security links', () => {
|
|||
it('returns false when neither the single OR capability is found nor all of the AND capabilities', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[siemShow, [readCases, createCases]],
|
||||
createCapabilities({
|
||||
siem: { show: false },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true },
|
||||
})
|
||||
}),
|
||||
[siemShow, [readCases, createCases]]
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
@ -373,11 +449,11 @@ describe('Security links', () => {
|
|||
it('returns true when the single OR capability is found when using an OR with an AND format', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[siemShow, [readCases, createCases]],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true },
|
||||
})
|
||||
}),
|
||||
[siemShow, [readCases, createCases]]
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -385,14 +461,14 @@ describe('Security links', () => {
|
|||
it("returns false when the AND'd expressions are not satisfied", () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[
|
||||
[siemShow, pushCases],
|
||||
[readCases, createCases],
|
||||
],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true, push_cases: false },
|
||||
})
|
||||
}),
|
||||
[
|
||||
[siemShow, pushCases],
|
||||
[readCases, createCases],
|
||||
]
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
|
@ -4,13 +4,11 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Capabilities } from '@kbn/core/public';
|
||||
import get from 'lodash/get';
|
||||
import { useMemo } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { SecurityPageName } from '../../../common/constants';
|
||||
import { hasCapabilities } from '../lib/capabilities';
|
||||
import type {
|
||||
AppLinkItems,
|
||||
LinkInfo,
|
||||
|
@ -40,9 +38,9 @@ export const updateAppLinks = (
|
|||
appLinksToUpdate: AppLinkItems,
|
||||
linksPermissions: LinksPermissions
|
||||
) => {
|
||||
const filteredAppLinks = getFilteredAppLinks(appLinksToUpdate, linksPermissions);
|
||||
appLinksUpdater$.next(Object.freeze(filteredAppLinks));
|
||||
normalizedAppLinksUpdater$.next(Object.freeze(getNormalizedLinks(filteredAppLinks)));
|
||||
const appLinks = processAppLinks(appLinksToUpdate, linksPermissions);
|
||||
appLinksUpdater$.next(Object.freeze(appLinks));
|
||||
normalizedAppLinksUpdater$.next(Object.freeze(getNormalizedLinks(appLinks)));
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -65,6 +63,28 @@ export const useLinkExists = (id: SecurityPageName): boolean => {
|
|||
return useMemo(() => !!normalizedLinks[id], [normalizedLinks, id]);
|
||||
};
|
||||
|
||||
export const useLinkInfo = (id: SecurityPageName): LinkInfo | undefined => {
|
||||
const normalizedLinks = useNormalizedAppLinks();
|
||||
return useMemo(() => {
|
||||
const normalizedLink = normalizedLinks[id];
|
||||
if (!normalizedLink) {
|
||||
return undefined;
|
||||
}
|
||||
// discards the parentId and creates the linkInfo copy.
|
||||
const { parentId, ...linkInfo } = normalizedLink;
|
||||
return linkInfo;
|
||||
}, [normalizedLinks, id]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if a link exists in the application links,
|
||||
* It can be used to know if a link access is authorized.
|
||||
*/
|
||||
export const useLinkAuthorized = (id: SecurityPageName): boolean => {
|
||||
const linkInfo = useLinkInfo(id);
|
||||
return useMemo(() => linkInfo != null && !linkInfo.unauthorized, [linkInfo]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the `LinkInfo` from a link id parameter
|
||||
*/
|
||||
|
@ -132,61 +152,31 @@ const getNormalizedLinks = (
|
|||
const getNormalizedLink = (id: SecurityPageName): Readonly<NormalizedLink> | undefined =>
|
||||
normalizedAppLinksUpdater$.getValue()[id];
|
||||
|
||||
const getFilteredAppLinks = (
|
||||
appLinkToFilter: AppLinkItems,
|
||||
linksPermissions: LinksPermissions
|
||||
): LinkItem[] =>
|
||||
appLinkToFilter.reduce<LinkItem[]>((acc, { links, ...appLink }) => {
|
||||
if (!isLinkAllowed(appLink, linksPermissions)) {
|
||||
const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissions): LinkItem[] =>
|
||||
appLinks.reduce<LinkItem[]>((acc, { links, ...appLinkWithoutSublinks }) => {
|
||||
if (!isLinkAllowed(appLinkWithoutSublinks, linksPermissions)) {
|
||||
return acc;
|
||||
}
|
||||
if (links) {
|
||||
const childrenLinks = getFilteredAppLinks(links, linksPermissions);
|
||||
if (childrenLinks.length > 0) {
|
||||
acc.push({ ...appLink, links: childrenLinks });
|
||||
} else {
|
||||
acc.push(appLink);
|
||||
if (!hasCapabilities(linksPermissions.capabilities, appLinkWithoutSublinks.capabilities)) {
|
||||
if (linksPermissions.upselling.isPageUpsellable(appLinkWithoutSublinks.id)) {
|
||||
acc.push({ ...appLinkWithoutSublinks, unauthorized: true });
|
||||
}
|
||||
} else {
|
||||
acc.push(appLink);
|
||||
return acc; // not adding sub-links for links that are not authorized
|
||||
}
|
||||
|
||||
const resultAppLink: LinkItem = appLinkWithoutSublinks;
|
||||
if (links) {
|
||||
const childrenLinks = processAppLinks(links, linksPermissions);
|
||||
if (childrenLinks.length > 0) {
|
||||
resultAppLink.links = childrenLinks;
|
||||
}
|
||||
}
|
||||
|
||||
acc.push(resultAppLink);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* The format of defining features supports OR and AND mechanism. To specify features in an OR fashion
|
||||
* they can be defined in a single level array like: [requiredFeature1, requiredFeature2]. If either of these features
|
||||
* is satisfied the links would be included. To require that the features be AND'd together a second level array
|
||||
* can be specified: [feature1, [feature2, feature3]] this would result in feature1 || (feature2 && feature3).
|
||||
*
|
||||
* The final format is to specify a single feature, this would be like: features: feature1, which is the same as
|
||||
* features: [feature1]
|
||||
*/
|
||||
type LinkCapabilities = string | Array<string | string[]>;
|
||||
|
||||
// It checks if the user has at least one of the link capabilities needed
|
||||
export const hasCapabilities = <T>(
|
||||
linkCapabilities: LinkCapabilities,
|
||||
userCapabilities: Capabilities
|
||||
): boolean => {
|
||||
if (!Array.isArray(linkCapabilities)) {
|
||||
return !!get(userCapabilities, linkCapabilities, false);
|
||||
} else {
|
||||
return linkCapabilities.some((linkCapabilityKeyOr) => {
|
||||
if (Array.isArray(linkCapabilityKeyOr)) {
|
||||
return linkCapabilityKeyOr.every((linkCapabilityKeyAnd) =>
|
||||
get(userCapabilities, linkCapabilityKeyAnd, false)
|
||||
);
|
||||
}
|
||||
return get(userCapabilities, linkCapabilityKeyOr, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isLinkAllowed = (
|
||||
link: LinkItem,
|
||||
{ license, experimentalFeatures, capabilities }: LinksPermissions
|
||||
) => {
|
||||
const isLinkAllowed = (link: LinkItem, { license, experimentalFeatures }: LinksPermissions) => {
|
||||
const linkLicenseType = link.licenseType ?? 'basic';
|
||||
if (license) {
|
||||
if (!license.hasAtLeast(linkLicenseType)) {
|
||||
|
@ -201,8 +191,5 @@ const isLinkAllowed = (
|
|||
if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) {
|
||||
return false;
|
||||
}
|
||||
if (link.capabilities && !hasCapabilities(link.capabilities, capabilities)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -10,6 +10,8 @@ import type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types';
|
|||
import type { IconType } from '@elastic/eui';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import type { SecurityPageName } from '../../../common/constants';
|
||||
import type { UpsellingService } from '../lib/upsellings';
|
||||
import type { RequiredCapabilities } from '../lib/capabilities';
|
||||
|
||||
/**
|
||||
* Permissions related parameters needed for the links to be filtered
|
||||
|
@ -17,6 +19,7 @@ import type { SecurityPageName } from '../../../common/constants';
|
|||
export interface LinksPermissions {
|
||||
capabilities: Capabilities;
|
||||
experimentalFeatures: Readonly<ExperimentalFeatures>;
|
||||
upselling: UpsellingService;
|
||||
license?: ILicense;
|
||||
}
|
||||
|
||||
|
@ -41,7 +44,7 @@ export interface LinkItem {
|
|||
* The final format is to specify a single feature, this would be like: features: feature1, which is the same as
|
||||
* features: [feature1]
|
||||
*/
|
||||
capabilities?: string | Array<string | string[]>;
|
||||
capabilities?: RequiredCapabilities;
|
||||
/**
|
||||
* Categories to display in the navigation
|
||||
*/
|
||||
|
@ -124,6 +127,10 @@ export interface LinkItem {
|
|||
* Title of the link
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Reserved for links management, this property is set automatically
|
||||
* */
|
||||
unauthorized?: boolean;
|
||||
}
|
||||
|
||||
export type AppLinkItems = Readonly<LinkItem[]>;
|
||||
|
@ -142,6 +149,7 @@ export interface NavigationLink {
|
|||
image?: string;
|
||||
title: string;
|
||||
skipUrlState?: boolean;
|
||||
unauthorized?: boolean;
|
||||
isBeta?: boolean;
|
||||
betaOptions?: {
|
||||
text: string;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { allowedExperimentalValues } from '../../../../common/experimental_features';
|
||||
import { UpsellingService } from '../../lib/upsellings';
|
||||
import { updateAppLinks } from '../../links';
|
||||
import { links } from '../../links/app_links';
|
||||
import { useShowTimeline } from './use_show_timeline';
|
||||
|
@ -51,6 +52,8 @@ jest.mock('../../lib/kibana', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const mockUpselling = new UpsellingService();
|
||||
|
||||
describe('use show timeline', () => {
|
||||
beforeAll(() => {
|
||||
// initialize all App links before running test
|
||||
|
@ -66,6 +69,7 @@ describe('use show timeline', () => {
|
|||
crud: true,
|
||||
},
|
||||
},
|
||||
upselling: mockUpselling,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import type { UsersComponentsQueryProps } from '../../../users/pages/navigation/
|
|||
import type { HostsComponentsQueryProps } from '../../../hosts/pages/navigation/types';
|
||||
import { useDashboardHref } from '../../../../common/hooks/use_dashboard_href';
|
||||
import { RiskScoresNoDataDetected } from '../risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useUpsellingComponent } from '../../../../common/hooks/use_upselling';
|
||||
|
||||
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
margin-top: ${({ theme }) => theme.eui.euiSizeL};
|
||||
|
@ -48,6 +49,7 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
riskEntity: RiskScoreEntity;
|
||||
}
|
||||
> = ({ entityName, startDate, endDate, setQuery, deleteQuery, riskEntity }) => {
|
||||
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
|
||||
const queryId = useMemo(
|
||||
() =>
|
||||
riskEntity === RiskScoreEntity.host
|
||||
|
@ -128,6 +130,10 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
isDeprecated: isDeprecated && !loading,
|
||||
};
|
||||
|
||||
if (RiskScoreUpsell) {
|
||||
return <RiskScoreUpsell />;
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import { EMPTY_SEVERITY_COUNT, RiskScoreEntity } from '../../../../../common/search_strategy';
|
||||
import { RiskScoresNoDataDetected } from '../../../components/risk_score/risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useUpsellingComponent } from '../../../../common/hooks/use_upselling';
|
||||
|
||||
const HostRiskScoreTableManage = manageQuery(HostRiskScoreTable);
|
||||
|
||||
|
@ -34,6 +35,7 @@ export const HostRiskScoreQueryTabBody = ({
|
|||
startDate: from,
|
||||
type,
|
||||
}: HostsComponentsQueryProps) => {
|
||||
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
|
||||
const getHostRiskScoreSelector = useMemo(() => hostsSelectors.hostRiskScoreSelector(), []);
|
||||
const { activePage, limit, sort } = useDeepEqualSelector((state: State) =>
|
||||
getHostRiskScoreSelector(state, hostsModel.HostsType.page)
|
||||
|
@ -90,6 +92,10 @@ export const HostRiskScoreQueryTabBody = ({
|
|||
isDeprecated: isDeprecated && !loading,
|
||||
};
|
||||
|
||||
if (RiskScoreUpsell) {
|
||||
return <RiskScoreUpsell />;
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import { EMPTY_SEVERITY_COUNT, RiskScoreEntity } from '../../../../../common/search_strategy';
|
||||
import { RiskScoresNoDataDetected } from '../../../components/risk_score/risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useUpsellingComponent } from '../../../../common/hooks/use_upselling';
|
||||
|
||||
const UserRiskScoreTableManage = manageQuery(UserRiskScoreTable);
|
||||
|
||||
|
@ -36,6 +37,8 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
startDate: from,
|
||||
type,
|
||||
}: UsersComponentsQueryProps) => {
|
||||
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
|
||||
|
||||
const getUserRiskScoreSelector = useMemo(() => usersSelectors.userRiskScoreSelector(), []);
|
||||
const { activePage, limit, sort } = useDeepEqualSelector((state: State) =>
|
||||
getUserRiskScoreSelector(state)
|
||||
|
@ -92,6 +95,10 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
isDeprecated: isDeprecated && !loading,
|
||||
};
|
||||
|
||||
if (RiskScoreUpsell) {
|
||||
return <RiskScoreUpsell />;
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
|
|
|
@ -23,6 +23,9 @@ import {
|
|||
noCasesCapabilities,
|
||||
readCasesCapabilities,
|
||||
} from './cases_test_utils';
|
||||
import { createStartServicesMock } from './common/lib/kibana/kibana_react.mock';
|
||||
|
||||
const mockServices = createStartServicesMock();
|
||||
|
||||
describe('public helpers parseRoute', () => {
|
||||
it('should properly parse hash route', () => {
|
||||
|
@ -74,15 +77,20 @@ describe('public helpers parseRoute', () => {
|
|||
|
||||
describe('#getSubPluginRoutesByCapabilities', () => {
|
||||
const mockRender = () => null;
|
||||
|
||||
const mockSubPlugins = {
|
||||
alerts: { routes: [{ path: 'alerts', render: mockRender }] },
|
||||
cases: { routes: [{ path: 'cases', render: mockRender }] },
|
||||
} as unknown as StartedSubPlugins;
|
||||
it('cases routes should return NoPrivilegesPage component when cases plugin is NOT available ', () => {
|
||||
const routes = getSubPluginRoutesByCapabilities(mockSubPlugins, {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities);
|
||||
const routes = getSubPluginRoutesByCapabilities(
|
||||
mockSubPlugins,
|
||||
{
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities,
|
||||
mockServices
|
||||
);
|
||||
const casesRoute = routes.find((r) => r.path === 'cases');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const CasesView = (casesRoute?.component ?? mockRender) as React.ComponentType<any>;
|
||||
|
@ -95,10 +103,14 @@ describe('#getSubPluginRoutesByCapabilities', () => {
|
|||
});
|
||||
|
||||
it('alerts should return NoPrivilegesPage component when siem plugin is NOT available ', () => {
|
||||
const routes = getSubPluginRoutesByCapabilities(mockSubPlugins, {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
} as unknown as Capabilities);
|
||||
const routes = getSubPluginRoutesByCapabilities(
|
||||
mockSubPlugins,
|
||||
{
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
} as unknown as Capabilities,
|
||||
mockServices
|
||||
);
|
||||
const alertsRoute = routes.find((r) => r.path === 'alerts');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const AlertsView = (alertsRoute?.component ?? mockRender) as React.ComponentType<any>;
|
||||
|
@ -111,10 +123,14 @@ describe('#getSubPluginRoutesByCapabilities', () => {
|
|||
});
|
||||
|
||||
it('should return NoPrivilegesPage for each route when both plugins are NOT available ', () => {
|
||||
const routes = getSubPluginRoutesByCapabilities(mockSubPlugins, {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities);
|
||||
const routes = getSubPluginRoutesByCapabilities(
|
||||
mockSubPlugins,
|
||||
{
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities,
|
||||
mockServices
|
||||
);
|
||||
const casesRoute = routes.find((r) => r.path === 'cases');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const CasesView = (casesRoute?.component ?? mockRender) as React.ComponentType<any>;
|
||||
|
@ -196,11 +212,11 @@ describe('#isSubPluginAvailable', () => {
|
|||
|
||||
describe('RedirectRoute', () => {
|
||||
it('RedirectRoute should redirect to overview page when siem and case privileges are all', () => {
|
||||
const mockCapabilitities = {
|
||||
const mockCapabilities = {
|
||||
[SERVER_APP_ID]: { show: true, crud: true },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/get_started"
|
||||
/>
|
||||
|
@ -208,11 +224,11 @@ describe('RedirectRoute', () => {
|
|||
});
|
||||
|
||||
it('RedirectRoute should redirect to overview page when siem and case privileges are read', () => {
|
||||
const mockCapabilitities = {
|
||||
const mockCapabilities = {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/get_started"
|
||||
/>
|
||||
|
@ -220,11 +236,11 @@ describe('RedirectRoute', () => {
|
|||
});
|
||||
|
||||
it('RedirectRoute should redirect to overview page when siem and case privileges are off', () => {
|
||||
const mockCapabilitities = {
|
||||
const mockCapabilities = {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/get_started"
|
||||
/>
|
||||
|
@ -232,11 +248,11 @@ describe('RedirectRoute', () => {
|
|||
});
|
||||
|
||||
it('RedirectRoute should redirect to overview page when siem privilege is read and case privilege is all', () => {
|
||||
const mockCapabilitities = {
|
||||
const mockCapabilities = {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/get_started"
|
||||
/>
|
||||
|
@ -244,11 +260,11 @@ describe('RedirectRoute', () => {
|
|||
});
|
||||
|
||||
it('RedirectRoute should redirect to overview page when siem privilege is read and case privilege is read', () => {
|
||||
const mockCapabilitities = {
|
||||
const mockCapabilities = {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/get_started"
|
||||
/>
|
||||
|
@ -256,11 +272,11 @@ describe('RedirectRoute', () => {
|
|||
});
|
||||
|
||||
it('RedirectRoute should redirect to cases page when siem privilege is none and case privilege is read', () => {
|
||||
const mockCapabilitities = {
|
||||
const mockCapabilities = {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/cases"
|
||||
/>
|
||||
|
@ -268,11 +284,11 @@ describe('RedirectRoute', () => {
|
|||
});
|
||||
|
||||
it('RedirectRoute should redirect to cases page when siem privilege is none and case privilege is all', () => {
|
||||
const mockCapabilitities = {
|
||||
const mockCapabilities = {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/cases"
|
||||
/>
|
||||
|
|
|
@ -33,7 +33,7 @@ import type {
|
|||
import type { TimelineEqlResponse } from '../common/search_strategy/timeline';
|
||||
import { NoPrivilegesPage } from './common/components/no_privileges';
|
||||
import { SecurityPageName } from './app/types';
|
||||
import type { InspectResponse, StartedSubPlugins } from './types';
|
||||
import type { InspectResponse, StartedSubPlugins, StartServices } from './types';
|
||||
import { CASES_SUB_PLUGIN_KEY } from './types';
|
||||
import { timelineActions } from './timelines/store/timeline';
|
||||
import { TimelineId } from '../common/types';
|
||||
|
@ -194,7 +194,8 @@ export const isThreatIntelligencePath = (pathname: string): boolean => {
|
|||
|
||||
export const getSubPluginRoutesByCapabilities = (
|
||||
subPlugins: StartedSubPlugins,
|
||||
capabilities: Capabilities
|
||||
capabilities: Capabilities,
|
||||
services: StartServices
|
||||
): RouteProps[] => {
|
||||
return [
|
||||
...Object.entries(subPlugins).reduce<RouteProps[]>((acc, [key, value]) => {
|
||||
|
@ -207,7 +208,13 @@ export const getSubPluginRoutesByCapabilities = (
|
|||
...acc,
|
||||
...value.routes.map((route: RouteProps) => ({
|
||||
path: route.path,
|
||||
component: () => <NoPrivilegesPage pageName={key} docLinkSelector={docLinkSelector} />,
|
||||
component: () => {
|
||||
const Upsell = services.upselling.getPageUpselling(key as SecurityPageName);
|
||||
if (Upsell) {
|
||||
return <Upsell />;
|
||||
}
|
||||
return <NoPrivilegesPage pageName={key} docLinkSelector={docLinkSelector} />;
|
||||
},
|
||||
})),
|
||||
];
|
||||
}, []),
|
||||
|
|
|
@ -9,6 +9,12 @@ import type { PluginInitializerContext } from '@kbn/core/public';
|
|||
import { Plugin } from './plugin';
|
||||
import type { PluginSetup, PluginStart } from './types';
|
||||
export type { TimelineModel } from './timelines/store/timeline/model';
|
||||
export type {
|
||||
UpsellingService,
|
||||
PageUpsellings,
|
||||
SectionUpsellings,
|
||||
UpsellingSectionId,
|
||||
} from './common/lib/upsellings';
|
||||
|
||||
export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context);
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ export const entityAnalyticsLinks: LinkItem = {
|
|||
'Entity analytics, anomalies, and threats to narrow down the monitoring surface area.',
|
||||
}),
|
||||
path: ENTITY_ANALYTICS_PATH,
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
capabilities: [`${SERVER_APP_ID}.entity-analytics`],
|
||||
isBeta: false,
|
||||
globalSearchKeywords: [ENTITY_ANALYTICS],
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import { DataQuality } from './pages/data_quality';
|
|||
import { DetectionResponse } from './pages/detection_response';
|
||||
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
|
||||
import { EntityAnalyticsPage } from './pages/entity_analytics';
|
||||
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
|
||||
|
||||
const OverviewRoutes = () => (
|
||||
<PluginTemplateWrapper>
|
||||
|
@ -48,9 +49,9 @@ const LandingRoutes = () => (
|
|||
|
||||
const EntityAnalyticsRoutes = () => (
|
||||
<PluginTemplateWrapper>
|
||||
<TrackApplicationView viewId={SecurityPageName.entityAnalytics}>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.entityAnalytics}>
|
||||
<EntityAnalyticsPage />
|
||||
</TrackApplicationView>
|
||||
</SecurityRoutePageWrapper>
|
||||
</PluginTemplateWrapper>
|
||||
);
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ import { getLazyEndpointPolicyResponseExtension } from './management/pages/polic
|
|||
import { getLazyEndpointGenericErrorsListExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list';
|
||||
import type { ExperimentalFeatures } from '../common/experimental_features';
|
||||
import { parseExperimentalConfigValue } from '../common/experimental_features';
|
||||
import { UpsellingService } from './common/lib/upsellings';
|
||||
import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension';
|
||||
|
||||
import type { SecurityAppStore } from './common/store/types';
|
||||
|
@ -79,6 +80,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
private telemetry: TelemetryService;
|
||||
|
||||
readonly experimentalFeatures: ExperimentalFeatures;
|
||||
private upsellingService: UpsellingService;
|
||||
private isSidebarEnabled$: BehaviorSubject<boolean>;
|
||||
private getStartedComponent?: GetStartedComponent;
|
||||
|
||||
|
@ -89,6 +91,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
this.kibanaBranch = initializerContext.env.packageInfo.branch;
|
||||
this.prebuiltRulesPackageVersion = this.config.prebuiltRulesPackageVersion;
|
||||
this.isSidebarEnabled$ = new BehaviorSubject<boolean>(true);
|
||||
this.upsellingService = new UpsellingService();
|
||||
this.telemetry = new TelemetryService();
|
||||
}
|
||||
private appUpdater$ = new Subject<AppUpdater>();
|
||||
|
@ -165,6 +168,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
savedObjectsManagement: startPluginsDeps.savedObjectsManagement,
|
||||
isSidebarEnabled$: this.isSidebarEnabled$,
|
||||
getStartedComponent: this.getStartedComponent,
|
||||
upselling: this.upsellingService,
|
||||
telemetry: this.telemetry.start(),
|
||||
};
|
||||
return services;
|
||||
|
@ -203,7 +207,8 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
usageCollection: plugins.usageCollection,
|
||||
subPluginRoutes: getSubPluginRoutesByCapabilities(
|
||||
subPlugins,
|
||||
coreStart.application.capabilities
|
||||
coreStart.application.capabilities,
|
||||
services
|
||||
),
|
||||
});
|
||||
},
|
||||
|
@ -239,6 +244,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
);
|
||||
return resolverPluginSetup();
|
||||
},
|
||||
upselling: this.upsellingService,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -491,6 +497,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
license$.subscribe(async (license) => {
|
||||
const linksPermissions: LinksPermissions = {
|
||||
experimentalFeatures: this.experimentalFeatures,
|
||||
upselling: this.upsellingService,
|
||||
capabilities: core.application.capabilities,
|
||||
};
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ import type { NavigationLink } from './common/links';
|
|||
|
||||
import type { TelemetryClientStart } from './common/lib/telemetry';
|
||||
import type { Dashboards } from './dashboards';
|
||||
import type { UpsellingService } from './common/lib/upsellings';
|
||||
|
||||
export interface SetupPlugins {
|
||||
cloud?: CloudSetup;
|
||||
|
@ -135,11 +136,13 @@ export type StartServices = CoreStart &
|
|||
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||
isSidebarEnabled$: BehaviorSubject<boolean>;
|
||||
getStartedComponent: GetStartedComponent | undefined;
|
||||
upselling: UpsellingService;
|
||||
telemetry: TelemetryClientStart;
|
||||
};
|
||||
|
||||
export interface PluginSetup {
|
||||
resolver: () => Promise<ResolverPluginSetup>;
|
||||
upselling: UpsellingService;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
|
|
|
@ -1,733 +0,0 @@
|
|||
/*
|
||||
* 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 { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
|
||||
import {
|
||||
createUICapabilities as createCasesUICapabilities,
|
||||
getApiTags as getCasesApiTags,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
|
||||
import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants';
|
||||
import { APP_ID, CASES_FEATURE_ID, SERVER_APP_ID } from '../common/constants';
|
||||
import { savedObjectTypes } from './saved_objects';
|
||||
import type { ConfigType } from './config';
|
||||
|
||||
export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
||||
const casesCapabilities = createCasesUICapabilities();
|
||||
const casesApiTags = getCasesApiTags(APP_ID);
|
||||
|
||||
return {
|
||||
id: CASES_FEATURE_ID,
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
api: casesApiTags.all,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.all,
|
||||
},
|
||||
read: {
|
||||
api: casesApiTags.read,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.read,
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: casesApiTags.delete,
|
||||
id: 'cases_delete',
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.deleteSubFeatureDetails',
|
||||
{
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
cases: {
|
||||
delete: [APP_ID],
|
||||
},
|
||||
ui: casesCapabilities.delete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
// Same as the plugin id defined by Cloud Security Posture
|
||||
const CLOUD_POSTURE_APP_ID = 'csp';
|
||||
// Same as the saved-object type for rules defined by Cloud Security Posture
|
||||
const CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE = 'csp_rule';
|
||||
|
||||
const responseActionSubFeatures: SubFeatureConfig[] = [
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Response Actions History access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory',
|
||||
{
|
||||
defaultMessage: 'Response Actions History',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.description',
|
||||
{
|
||||
defaultMessage: 'Access the history of response actions performed on endpoints.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeActionsLogManagement`, `${APP_ID}-readActionsLogManagement`],
|
||||
id: 'actions_log_management_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeActionsLogManagement', 'readActionsLogManagement'],
|
||||
},
|
||||
{
|
||||
api: [`${APP_ID}-readActionsLogManagement`],
|
||||
id: 'actions_log_management_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readActionsLogManagement'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Host Isolation access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.hostIsolation', {
|
||||
defaultMessage: 'Host Isolation',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.description',
|
||||
{ defaultMessage: 'Perform the "isolate" and "release" response actions.' }
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeHostIsolation`],
|
||||
id: 'host_isolation_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeHostIsolation'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.processOperations.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Process Operations access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.processOperations', {
|
||||
defaultMessage: 'Process Operations',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.processOperations.description',
|
||||
{
|
||||
defaultMessage: 'Perform process-related response actions in the response console.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeProcessOperations`],
|
||||
id: 'process_operations_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeProcessOperations'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.fileOperations.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for File Operations access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.fileOperations', {
|
||||
defaultMessage: 'File Operations',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.fileOperations.description',
|
||||
{
|
||||
defaultMessage: 'Perform file-related response actions in the response console.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeFileOperations`],
|
||||
id: 'file_operations_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeFileOperations'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const subFeatures: SubFeatureConfig[] = [
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.endpointList.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Endpoint List access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.endpointList', {
|
||||
defaultMessage: 'Endpoint List',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.endpointList.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Displays all hosts running Elastic Defend and their relevant integration details.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeEndpointList`, `${APP_ID}-readEndpointList`],
|
||||
id: 'endpoint_list_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeEndpointList', 'readEndpointList'],
|
||||
},
|
||||
{
|
||||
api: [`${APP_ID}-readEndpointList`],
|
||||
id: 'endpoint_list_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readEndpointList'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Trusted Applications access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.trustedApplications', {
|
||||
defaultMessage: 'Trusted Applications',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
`${APP_ID}-writeTrustedApplications`,
|
||||
`${APP_ID}-readTrustedApplications`,
|
||||
],
|
||||
id: 'trusted_applications_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeTrustedApplications', 'readTrustedApplications'],
|
||||
},
|
||||
{
|
||||
api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedApplications`],
|
||||
id: 'trusted_applications_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readTrustedApplications'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions',
|
||||
{
|
||||
defaultMessage: 'Host Isolation Exceptions',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
`${APP_ID}-writeHostIsolationExceptions`,
|
||||
`${APP_ID}-readHostIsolationExceptions`,
|
||||
],
|
||||
id: 'host_isolation_exceptions_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeHostIsolationExceptions', 'readHostIsolationExceptions'],
|
||||
},
|
||||
{
|
||||
api: ['lists-read', 'lists-summary', `${APP_ID}-readHostIsolationExceptions`],
|
||||
id: 'host_isolation_exceptions_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readHostIsolationExceptions'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.blockList.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Blocklist access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.blockList', {
|
||||
defaultMessage: 'Blocklist',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.blockList.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
`${APP_ID}-writeBlocklist`,
|
||||
`${APP_ID}-readBlocklist`,
|
||||
],
|
||||
id: 'blocklist_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeBlocklist', 'readBlocklist'],
|
||||
},
|
||||
{
|
||||
api: ['lists-read', 'lists-summary', `${APP_ID}-readBlocklist`],
|
||||
id: 'blocklist_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readBlocklist'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Event Filters access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.eventFilters', {
|
||||
defaultMessage: 'Event Filters',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Filter out endpoint events that you do not need or want stored in Elasticsearch.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
`${APP_ID}-writeEventFilters`,
|
||||
`${APP_ID}-readEventFilters`,
|
||||
],
|
||||
id: 'event_filters_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeEventFilters', 'readEventFilters'],
|
||||
},
|
||||
{
|
||||
api: ['lists-read', 'lists-summary', `${APP_ID}-readEventFilters`],
|
||||
id: 'event_filters_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readEventFilters'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Policy Management access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.policyManagement', {
|
||||
defaultMessage: 'Elastic Defend Policy Management',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writePolicyManagement`, `${APP_ID}-readPolicyManagement`],
|
||||
id: 'policy_management_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writePolicyManagement', 'readPolicyManagement'],
|
||||
},
|
||||
{
|
||||
api: [`${APP_ID}-readPolicyManagement`],
|
||||
id: 'policy_management_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readPolicyManagement'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...responseActionSubFeatures,
|
||||
];
|
||||
|
||||
// execute operations are not available in 8.7,
|
||||
// but will be available in 8.8
|
||||
const executeActionSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.executeOperations.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Execute Operations access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.executeOperations', {
|
||||
defaultMessage: 'Execute Operations',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.executeOperations.description',
|
||||
{
|
||||
// TODO: Update this description before 8.8 FF
|
||||
defaultMessage: 'Perform script execution on the endpoint.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeExecuteOperations`],
|
||||
id: 'execute_operations_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeExecuteOperations'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function getSubFeatures(experimentalFeatures: ConfigType['experimentalFeatures']) {
|
||||
let filteredSubFeatures: SubFeatureConfig[] = [];
|
||||
|
||||
if (experimentalFeatures.endpointRbacEnabled) {
|
||||
filteredSubFeatures = subFeatures;
|
||||
} else if (experimentalFeatures.endpointRbacV1Enabled) {
|
||||
filteredSubFeatures = responseActionSubFeatures;
|
||||
}
|
||||
|
||||
if (!experimentalFeatures.responseActionGetFileEnabled) {
|
||||
filteredSubFeatures = filteredSubFeatures.filter((subFeat) => {
|
||||
return subFeat.name !== 'File Operations';
|
||||
});
|
||||
}
|
||||
|
||||
// behind FF (planned for 8.8)
|
||||
if (experimentalFeatures.responseActionExecuteEnabled) {
|
||||
filteredSubFeatures = [...filteredSubFeatures, executeActionSubFeature];
|
||||
}
|
||||
|
||||
return filteredSubFeatures;
|
||||
}
|
||||
|
||||
export const getKibanaPrivilegesFeaturePrivileges = (
|
||||
ruleTypes: string[],
|
||||
experimentalFeatures: ConfigType['experimentalFeatures']
|
||||
): KibanaFeatureConfig => ({
|
||||
id: SERVER_APP_ID,
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle', {
|
||||
defaultMessage: 'Security',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: ruleTypes,
|
||||
privileges: {
|
||||
all: {
|
||||
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
api: [
|
||||
APP_ID,
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
'rac',
|
||||
'cloud-security-posture-all',
|
||||
'cloud-security-posture-read',
|
||||
],
|
||||
savedObject: {
|
||||
all: [
|
||||
'alert',
|
||||
'exception-list',
|
||||
EXCEPTION_LIST_NAMESPACE_AGNOSTIC,
|
||||
DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
...savedObjectTypes,
|
||||
CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE,
|
||||
],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ruleTypes,
|
||||
},
|
||||
alert: {
|
||||
all: ruleTypes,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'crud'],
|
||||
},
|
||||
read: {
|
||||
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
api: [APP_ID, 'lists-read', 'rac', 'cloud-security-posture-read'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [
|
||||
'exception-list',
|
||||
EXCEPTION_LIST_NAMESPACE_AGNOSTIC,
|
||||
DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
...savedObjectTypes,
|
||||
CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE,
|
||||
],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: ruleTypes,
|
||||
},
|
||||
alert: {
|
||||
all: ruleTypes,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: getSubFeatures(experimentalFeatures),
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { AppFeatures } from '.';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { AppFeatureKeys, ExperimentalFeatures } from '../../../common';
|
||||
import type { PluginSetupContract } from '@kbn/features-plugin/server';
|
||||
|
||||
const SECURITY_BASE_CONFIG = {
|
||||
foo: 'foo',
|
||||
};
|
||||
|
||||
const SECURITY_APP_FEATURE_CONFIG = {
|
||||
'test-base-feature': {
|
||||
privileges: {
|
||||
all: {
|
||||
ui: ['test-capability'],
|
||||
api: ['test-capability'],
|
||||
},
|
||||
read: {
|
||||
ui: ['test-capability'],
|
||||
api: ['test-capability'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const CASES_BASE_CONFIG = {
|
||||
bar: 'bar',
|
||||
};
|
||||
|
||||
const CASES_APP_FEATURE_CONFIG = {
|
||||
'test-cases-feature': {
|
||||
privileges: {
|
||||
all: {
|
||||
ui: ['test-cases-capability'],
|
||||
api: ['test-cases-capability'],
|
||||
},
|
||||
read: {
|
||||
ui: ['test-cases-capability'],
|
||||
api: ['test-cases-capability'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('./security_kibana_features', () => {
|
||||
return {
|
||||
getSecurityBaseKibanaFeature: jest.fn().mockReturnValue(SECURITY_BASE_CONFIG),
|
||||
getSecurityAppFeaturesConfig: jest.fn().mockReturnValue(SECURITY_APP_FEATURE_CONFIG),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./security_cases_kibana_features', () => {
|
||||
return {
|
||||
getCasesBaseKibanaFeature: jest.fn().mockReturnValue(CASES_BASE_CONFIG),
|
||||
getCasesAppFeaturesConfig: jest.fn().mockReturnValue(CASES_APP_FEATURE_CONFIG),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AppFeatures', () => {
|
||||
it('should register enabled kibana features', () => {
|
||||
const featuresSetup = {
|
||||
registerKibanaFeature: jest.fn(),
|
||||
getKibanaFeatures: jest.fn(),
|
||||
} as unknown as PluginSetupContract;
|
||||
|
||||
const appFeatureKeys = {
|
||||
'test-base-feature': true,
|
||||
} as unknown as AppFeatureKeys;
|
||||
|
||||
const appFeatures = new AppFeatures(
|
||||
{} as unknown as Logger,
|
||||
[] as unknown as ExperimentalFeatures
|
||||
);
|
||||
appFeatures.init(featuresSetup);
|
||||
appFeatures.set(appFeatureKeys);
|
||||
|
||||
expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({
|
||||
...SECURITY_BASE_CONFIG,
|
||||
...SECURITY_APP_FEATURE_CONFIG['test-base-feature'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should register enabled cases features', () => {
|
||||
const featuresSetup = {
|
||||
registerKibanaFeature: jest.fn(),
|
||||
} as unknown as PluginSetupContract;
|
||||
|
||||
const appFeatureKeys = {
|
||||
'test-cases-feature': true,
|
||||
} as unknown as AppFeatureKeys;
|
||||
|
||||
const appFeatures = new AppFeatures(
|
||||
{} as unknown as Logger,
|
||||
[] as unknown as ExperimentalFeatures
|
||||
);
|
||||
appFeatures.init(featuresSetup);
|
||||
appFeatures.set(appFeatureKeys);
|
||||
|
||||
expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({
|
||||
...CASES_BASE_CONFIG,
|
||||
...CASES_APP_FEATURE_CONFIG['test-cases-feature'],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 type { Logger } from '@kbn/core/server';
|
||||
import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||
import type { AppFeatureKey, AppFeatureKeys, ExperimentalFeatures } from '../../../common';
|
||||
import type { AppFeatureKibanaConfig, AppFeaturesConfig } from './types';
|
||||
import {
|
||||
getSecurityAppFeaturesConfig,
|
||||
getSecurityBaseKibanaFeature,
|
||||
} from './security_kibana_features';
|
||||
import {
|
||||
getCasesBaseKibanaFeature,
|
||||
getCasesAppFeaturesConfig,
|
||||
} from './security_cases_kibana_features';
|
||||
import { AppFeaturesConfigMerger } from './app_features_config_merger';
|
||||
|
||||
type AppFeaturesMap = Map<AppFeatureKey, boolean>;
|
||||
|
||||
export class AppFeatures {
|
||||
private merger: AppFeaturesConfigMerger;
|
||||
private appFeatures?: AppFeaturesMap;
|
||||
private featuresSetup?: FeaturesPluginSetup;
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly experimentalFeatures: ExperimentalFeatures
|
||||
) {
|
||||
this.merger = new AppFeaturesConfigMerger(this.logger);
|
||||
}
|
||||
|
||||
public init(featuresSetup: FeaturesPluginSetup) {
|
||||
this.featuresSetup = featuresSetup;
|
||||
}
|
||||
|
||||
public set(appFeatureKeys: AppFeatureKeys) {
|
||||
if (this.appFeatures) {
|
||||
throw new Error('AppFeatures has already been initialized');
|
||||
}
|
||||
this.appFeatures = new Map(Object.entries(appFeatureKeys) as Array<[AppFeatureKey, boolean]>);
|
||||
this.registerEnabledKibanaFeatures();
|
||||
}
|
||||
|
||||
public isEnabled(appFeatureKey: AppFeatureKey): boolean {
|
||||
if (!this.appFeatures) {
|
||||
throw new Error('AppFeatures has not been initialized');
|
||||
}
|
||||
return this.appFeatures.get(appFeatureKey) ?? false;
|
||||
}
|
||||
|
||||
private registerEnabledKibanaFeatures() {
|
||||
if (this.featuresSetup == null) {
|
||||
throw new Error(
|
||||
'Cannot sync kibana features as featuresSetup is not present. Did you call init?'
|
||||
);
|
||||
}
|
||||
// register main security Kibana features
|
||||
const securityBaseKibanaFeature = getSecurityBaseKibanaFeature(this.experimentalFeatures);
|
||||
const enabledSecurityAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
|
||||
getSecurityAppFeaturesConfig()
|
||||
);
|
||||
this.featuresSetup.registerKibanaFeature(
|
||||
this.merger.mergeAppFeatureConfigs(
|
||||
securityBaseKibanaFeature,
|
||||
enabledSecurityAppFeaturesConfigs
|
||||
)
|
||||
);
|
||||
|
||||
// register security cases Kibana features
|
||||
const securityCasesBaseKibanaFeature = getCasesBaseKibanaFeature();
|
||||
const enabledCasesAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
|
||||
getCasesAppFeaturesConfig()
|
||||
);
|
||||
this.featuresSetup.registerKibanaFeature(
|
||||
this.merger.mergeAppFeatureConfigs(
|
||||
securityCasesBaseKibanaFeature,
|
||||
enabledCasesAppFeaturesConfigs
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private getEnabledAppFeaturesConfigs(
|
||||
appFeaturesConfigs: Partial<AppFeaturesConfig>
|
||||
): AppFeatureKibanaConfig[] {
|
||||
return Object.entries(appFeaturesConfigs).reduce<AppFeatureKibanaConfig[]>(
|
||||
(acc, [appFeatureKey, appFeatureConfig]) => {
|
||||
if (this.isEnabled(appFeatureKey as AppFeatureKey)) {
|
||||
acc.push(appFeatureConfig);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { AppFeaturesConfigMerger } from './app_features_config_merger';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { AppFeatureKibanaConfig } from './types';
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
|
||||
const mockLogger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
describe('AppFeaturesConfigMerger', () => {
|
||||
// We don't need to update this test when cases config change
|
||||
// It mocks simplified versions of cases config
|
||||
it('merges a mocked version of cases config', () => {
|
||||
const merger = new AppFeaturesConfigMerger(mockLogger);
|
||||
|
||||
const category = {
|
||||
id: 'security',
|
||||
label: 'Security app category',
|
||||
};
|
||||
|
||||
const securityCasesBaseKibanaFeature: KibanaFeatureConfig = {
|
||||
id: 'CASES_FEATURE_ID',
|
||||
name: 'Cases',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
api: [],
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
read: {
|
||||
api: [],
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const enabledCasesAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
cases: ['APP_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['casesApiTags.all'],
|
||||
ui: ['casesCapabilities.all'],
|
||||
cases: {
|
||||
create: ['APP_ID'],
|
||||
read: ['APP_ID'],
|
||||
update: ['APP_ID'],
|
||||
push: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['filesSavedObjectTypes'],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: ['casesApiTags.read'],
|
||||
ui: ['casesCapabilities.read'],
|
||||
cases: {
|
||||
read: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
},
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Delete',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: ['casesApiTags.delete'],
|
||||
id: 'cases_delete',
|
||||
name: 'Delete cases and comments',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['filesSavedObjectTypes'],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
},
|
||||
cases: {
|
||||
delete: ['APP_ID'],
|
||||
},
|
||||
ui: ['casesCapabilities.delete'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const merged = merger.mergeAppFeatureConfigs(
|
||||
securityCasesBaseKibanaFeature,
|
||||
enabledCasesAppFeaturesConfigs
|
||||
);
|
||||
|
||||
expect(merged).toEqual({
|
||||
id: 'CASES_FEATURE_ID',
|
||||
name: 'Cases',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
cases: ['APP_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['casesApiTags.all'],
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
cases: {
|
||||
create: ['APP_ID'],
|
||||
read: ['APP_ID'],
|
||||
update: ['APP_ID'],
|
||||
push: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['filesSavedObjectTypes'],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
},
|
||||
ui: ['casesCapabilities.all'],
|
||||
},
|
||||
read: {
|
||||
api: ['casesApiTags.read'],
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
cases: {
|
||||
read: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
},
|
||||
ui: ['casesCapabilities.read'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Delete',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: ['casesApiTags.delete'],
|
||||
id: 'cases_delete',
|
||||
name: 'Delete cases and comments',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['filesSavedObjectTypes'],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
},
|
||||
cases: {
|
||||
delete: ['APP_ID'],
|
||||
},
|
||||
ui: ['casesCapabilities.delete'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('merges a mocked version of security basic config', () => {
|
||||
const merger = new AppFeaturesConfigMerger(mockLogger);
|
||||
|
||||
const category = {
|
||||
id: 'security',
|
||||
label: 'Security app category',
|
||||
};
|
||||
|
||||
const securityCasesBaseKibanaFeature: KibanaFeatureConfig = {
|
||||
id: 'SERVER_APP_ID',
|
||||
name: 'Security',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: ['THRESHOLD_RULE_TYPE_ID', 'NEW_TERMS_RULE_TYPE_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
api: ['APP_ID', 'cloud-security-posture-read'],
|
||||
savedObject: {
|
||||
all: ['alert', 'CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE'],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
alert: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'crud'],
|
||||
},
|
||||
read: {
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
api: ['APP_ID', 'lists-read', 'rac', 'cloud-security-posture-read'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE'],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
alert: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: 'All Spaces is required for Host Isolation access.',
|
||||
name: 'Host Isolation',
|
||||
description: 'Perform the "isolate" and "release" response actions.',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`APP_ID-writeHostIsolation`],
|
||||
id: 'host_isolation_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeHostIsolation'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enabledCasesAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['rules_load_prepackaged'],
|
||||
ui: ['rules_load_prepackaged'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const merged = merger.mergeAppFeatureConfigs(
|
||||
securityCasesBaseKibanaFeature,
|
||||
enabledCasesAppFeaturesConfigs
|
||||
);
|
||||
|
||||
expect(merged).toEqual({
|
||||
id: 'SERVER_APP_ID',
|
||||
name: 'Security',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: ['THRESHOLD_RULE_TYPE_ID', 'NEW_TERMS_RULE_TYPE_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
api: ['APP_ID', 'cloud-security-posture-read', 'rules_load_prepackaged'],
|
||||
savedObject: {
|
||||
all: ['alert', 'CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE'],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
alert: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'crud', 'rules_load_prepackaged'],
|
||||
},
|
||||
read: {
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
api: ['APP_ID', 'lists-read', 'rac', 'cloud-security-posture-read'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE'],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
alert: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: 'All Spaces is required for Host Isolation access.',
|
||||
name: 'Host Isolation',
|
||||
description: 'Perform the "isolate" and "release" response actions.',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`APP_ID-writeHostIsolation`],
|
||||
id: 'host_isolation_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeHostIsolation'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { cloneDeep, mergeWith, isArray, uniq } from 'lodash';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import type { AppFeatureKibanaConfig, SubFeaturesPrivileges } from './types';
|
||||
|
||||
export class AppFeaturesConfigMerger {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
/**
|
||||
* Merges `appFeaturesConfigs` into `kibanaFeatureConfig`.
|
||||
* @param kibanaFeatureConfig the KibanaFeatureConfig to merge into
|
||||
* @param appFeaturesConfigs the AppFeatureKibanaConfig to merge
|
||||
* @returns mergedKibanaFeatureConfig the merged KibanaFeatureConfig
|
||||
* */
|
||||
public mergeAppFeatureConfigs(
|
||||
kibanaFeatureConfig: KibanaFeatureConfig,
|
||||
appFeaturesConfigs: AppFeatureKibanaConfig[]
|
||||
): KibanaFeatureConfig {
|
||||
const mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig);
|
||||
const subFeaturesPrivilegesToMerge: SubFeaturesPrivileges[] = [];
|
||||
|
||||
appFeaturesConfigs.forEach((appFeatureConfig) => {
|
||||
const { subFeaturesPrivileges, ...appFeatureConfigToMerge } = cloneDeep(appFeatureConfig);
|
||||
if (subFeaturesPrivileges) {
|
||||
subFeaturesPrivilegesToMerge.push(...subFeaturesPrivileges);
|
||||
}
|
||||
mergeWith(mergedKibanaFeatureConfig, appFeatureConfigToMerge, featureConfigMerger);
|
||||
});
|
||||
|
||||
// add subFeaturePrivileges at the end to make sure all enabled subFeatures are merged
|
||||
subFeaturesPrivilegesToMerge.forEach((subFeaturesPrivileges) => {
|
||||
this.mergeSubFeaturesPrivileges(mergedKibanaFeatureConfig.subFeatures, subFeaturesPrivileges);
|
||||
});
|
||||
|
||||
return mergedKibanaFeatureConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges `subFeaturesPrivileges` into `kibanaFeatureConfig.subFeatures` by finding the subFeature privilege id.
|
||||
* @param subFeatures the subFeatures to merge into
|
||||
* @param subFeaturesPrivileges the subFeaturesPrivileges to merge
|
||||
* @returns void
|
||||
* */
|
||||
private mergeSubFeaturesPrivileges(
|
||||
subFeatures: KibanaFeatureConfig['subFeatures'],
|
||||
subFeaturesPrivileges: SubFeaturesPrivileges
|
||||
): void {
|
||||
if (!subFeatures) {
|
||||
this.logger.warn('Trying to merge subFeaturesPrivileges but no subFeatures found');
|
||||
return;
|
||||
}
|
||||
const merged = subFeatures.find(({ privilegeGroups }) =>
|
||||
privilegeGroups.some(({ privileges }) => {
|
||||
const subFeaturePrivilegeToUpdate = privileges.find(
|
||||
({ id }) => id === subFeaturesPrivileges.id
|
||||
);
|
||||
if (subFeaturePrivilegeToUpdate) {
|
||||
mergeWith(subFeaturePrivilegeToUpdate, subFeaturesPrivileges, featureConfigMerger);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
);
|
||||
if (!merged) {
|
||||
this.logger.warn(
|
||||
`Trying to merge subFeaturesPrivileges ${subFeaturesPrivileges.id} but the subFeature privilege was not found`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The customizer used by lodash.mergeWith to merge deep objects
|
||||
* Uses concatenation for arrays and removes duplicates, objects are merged using lodash.mergeWith default behavior
|
||||
* */
|
||||
function featureConfigMerger(objValue: unknown, srcValue: unknown) {
|
||||
if (isArray(srcValue)) {
|
||||
if (isArray(objValue)) {
|
||||
return uniq(objValue.concat(srcValue));
|
||||
}
|
||||
return srcValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { AppFeatures } from './app_features';
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import {
|
||||
createUICapabilities as createCasesUICapabilities,
|
||||
getApiTags as getCasesApiTags,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
import type { AppFeaturesCasesConfig } from './types';
|
||||
import { APP_ID, CASES_FEATURE_ID } from '../../../common/constants';
|
||||
import { casesSubFeatureDelete } from './security_cases_kibana_sub_features';
|
||||
import { AppFeatureCasesKey } from '../../../common/types/app_features';
|
||||
|
||||
const casesCapabilities = createCasesUICapabilities();
|
||||
const casesApiTags = getCasesApiTags(APP_ID);
|
||||
|
||||
export const getCasesBaseKibanaFeature = (): KibanaFeatureConfig => ({
|
||||
id: CASES_FEATURE_ID,
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
api: casesApiTags.all,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.all,
|
||||
},
|
||||
read: {
|
||||
api: casesApiTags.read,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.read,
|
||||
},
|
||||
},
|
||||
subFeatures: [casesSubFeatureDelete],
|
||||
});
|
||||
|
||||
// It maps the AppFeatures keys to Kibana privileges
|
||||
export const getCasesAppFeaturesConfig = (): AppFeaturesCasesConfig => ({
|
||||
// TODO Add cases connector configuration
|
||||
[AppFeatureCasesKey.casesConnectors]: {},
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import type { SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import {
|
||||
createUICapabilities as createCasesUICapabilities,
|
||||
getApiTags as getCasesApiTags,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
import { APP_ID } from '../../../common/constants';
|
||||
|
||||
const casesCapabilities = createCasesUICapabilities();
|
||||
const casesApiTags = getCasesApiTags(APP_ID);
|
||||
|
||||
export const casesSubFeatureDelete: SubFeatureConfig = {
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: casesApiTags.delete,
|
||||
id: 'cases_delete',
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.deleteSubFeatureDetails', {
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
cases: {
|
||||
delete: [APP_ID],
|
||||
},
|
||||
ui: casesCapabilities.delete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* 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 type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
|
||||
import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants';
|
||||
import {
|
||||
EQL_RULE_TYPE_ID,
|
||||
INDICATOR_RULE_TYPE_ID,
|
||||
ML_RULE_TYPE_ID,
|
||||
NEW_TERMS_RULE_TYPE_ID,
|
||||
QUERY_RULE_TYPE_ID,
|
||||
SAVED_QUERY_RULE_TYPE_ID,
|
||||
THRESHOLD_RULE_TYPE_ID,
|
||||
} from '@kbn/securitysolution-rules';
|
||||
import { APP_ID, LEGACY_NOTIFICATIONS_ID, SERVER_APP_ID } from '../../../common/constants';
|
||||
import { savedObjectTypes } from '../../saved_objects';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import {
|
||||
blocklistSubFeature,
|
||||
endpointListSubFeature,
|
||||
eventFiltersSubFeature,
|
||||
executeActionSubFeature,
|
||||
fileOperationsSubFeature,
|
||||
hostIsolationExceptionsSubFeature,
|
||||
hostIsolationSubFeature,
|
||||
policyManagementSubFeature,
|
||||
processOperationsSubFeature,
|
||||
responseActionsHistorySubFeature,
|
||||
trustedApplicationsSubFeature,
|
||||
} from './security_kibana_sub_features';
|
||||
import type { AppFeaturesSecurityConfig } from './types';
|
||||
import { AppFeatureSecurityKey } from '../../../common/types/app_features';
|
||||
|
||||
// Same as the plugin id defined by Cloud Security Posture
|
||||
const CLOUD_POSTURE_APP_ID = 'csp';
|
||||
// Same as the saved-object type for rules defined by Cloud Security Posture
|
||||
const CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE = 'csp_rule';
|
||||
|
||||
const SECURITY_RULE_TYPES = [
|
||||
LEGACY_NOTIFICATIONS_ID,
|
||||
EQL_RULE_TYPE_ID,
|
||||
INDICATOR_RULE_TYPE_ID,
|
||||
ML_RULE_TYPE_ID,
|
||||
QUERY_RULE_TYPE_ID,
|
||||
SAVED_QUERY_RULE_TYPE_ID,
|
||||
THRESHOLD_RULE_TYPE_ID,
|
||||
NEW_TERMS_RULE_TYPE_ID,
|
||||
];
|
||||
|
||||
export const getSecurityBaseKibanaFeature = (
|
||||
experimentalFeatures: ExperimentalFeatures
|
||||
): KibanaFeatureConfig => ({
|
||||
id: SERVER_APP_ID,
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle', {
|
||||
defaultMessage: 'Security',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: SECURITY_RULE_TYPES,
|
||||
privileges: {
|
||||
all: {
|
||||
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
api: [
|
||||
APP_ID,
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
'rac',
|
||||
'cloud-security-posture-all',
|
||||
'cloud-security-posture-read',
|
||||
],
|
||||
savedObject: {
|
||||
all: [
|
||||
'alert',
|
||||
'exception-list',
|
||||
EXCEPTION_LIST_NAMESPACE_AGNOSTIC,
|
||||
DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
...savedObjectTypes,
|
||||
CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE,
|
||||
],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: SECURITY_RULE_TYPES,
|
||||
},
|
||||
alert: {
|
||||
all: SECURITY_RULE_TYPES,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'crud'],
|
||||
},
|
||||
read: {
|
||||
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
api: [APP_ID, 'lists-read', 'rac', 'cloud-security-posture-read'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [
|
||||
'exception-list',
|
||||
EXCEPTION_LIST_NAMESPACE_AGNOSTIC,
|
||||
DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
...savedObjectTypes,
|
||||
CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE,
|
||||
],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: SECURITY_RULE_TYPES,
|
||||
},
|
||||
alert: {
|
||||
all: SECURITY_RULE_TYPES,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: getSubFeatures(experimentalFeatures),
|
||||
});
|
||||
|
||||
function getSubFeatures(experimentalFeatures: ExperimentalFeatures) {
|
||||
const subFeatures: SubFeatureConfig[] = [];
|
||||
|
||||
if (experimentalFeatures.endpointRbacEnabled) {
|
||||
subFeatures.push(
|
||||
endpointListSubFeature,
|
||||
trustedApplicationsSubFeature,
|
||||
hostIsolationExceptionsSubFeature,
|
||||
blocklistSubFeature,
|
||||
eventFiltersSubFeature,
|
||||
policyManagementSubFeature
|
||||
);
|
||||
}
|
||||
|
||||
if (experimentalFeatures.endpointRbacEnabled || experimentalFeatures.endpointRbacV1Enabled) {
|
||||
subFeatures.push(
|
||||
responseActionsHistorySubFeature,
|
||||
hostIsolationSubFeature,
|
||||
processOperationsSubFeature
|
||||
);
|
||||
}
|
||||
if (experimentalFeatures.responseActionGetFileEnabled) {
|
||||
subFeatures.push(fileOperationsSubFeature);
|
||||
}
|
||||
// planned for 8.8
|
||||
if (experimentalFeatures.responseActionExecuteEnabled) {
|
||||
subFeatures.push(executeActionSubFeature);
|
||||
}
|
||||
|
||||
return subFeatures;
|
||||
}
|
||||
|
||||
// maps the AppFeatures keys to Kibana privileges
|
||||
export const getSecurityAppFeaturesConfig = (): AppFeaturesSecurityConfig => {
|
||||
return {
|
||||
[AppFeatureSecurityKey.advancedInsights]: {
|
||||
privileges: {
|
||||
all: {
|
||||
ui: ['entity-analytics'],
|
||||
api: [`${APP_ID}-entity-analytics`],
|
||||
},
|
||||
read: {
|
||||
ui: ['entity-analytics'],
|
||||
api: [`${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,525 @@
|
|||
/*
|
||||
* 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 type { SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants';
|
||||
import { APP_ID } from '../../../common';
|
||||
|
||||
export const endpointListSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.endpointList.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Endpoint List access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.endpointList', {
|
||||
defaultMessage: 'Endpoint List',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.endpointList.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Displays all hosts running Elastic Defend and their relevant integration details.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeEndpointList`, `${APP_ID}-readEndpointList`],
|
||||
id: 'endpoint_list_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeEndpointList', 'readEndpointList'],
|
||||
},
|
||||
{
|
||||
api: [`${APP_ID}-readEndpointList`],
|
||||
id: 'endpoint_list_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readEndpointList'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const trustedApplicationsSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Trusted Applications access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.trustedApplications', {
|
||||
defaultMessage: 'Trusted Applications',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
`${APP_ID}-writeTrustedApplications`,
|
||||
`${APP_ID}-readTrustedApplications`,
|
||||
],
|
||||
id: 'trusted_applications_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeTrustedApplications', 'readTrustedApplications'],
|
||||
},
|
||||
{
|
||||
api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedApplications`],
|
||||
id: 'trusted_applications_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readTrustedApplications'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const hostIsolationExceptionsSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions',
|
||||
{
|
||||
defaultMessage: 'Host Isolation Exceptions',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
`${APP_ID}-writeHostIsolationExceptions`,
|
||||
`${APP_ID}-readHostIsolationExceptions`,
|
||||
],
|
||||
id: 'host_isolation_exceptions_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeHostIsolationExceptions', 'readHostIsolationExceptions'],
|
||||
},
|
||||
{
|
||||
api: ['lists-read', 'lists-summary', `${APP_ID}-readHostIsolationExceptions`],
|
||||
id: 'host_isolation_exceptions_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readHostIsolationExceptions'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const blocklistSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.blockList.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Blocklist access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.blockList', {
|
||||
defaultMessage: 'Blocklist',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.blockList.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
`${APP_ID}-writeBlocklist`,
|
||||
`${APP_ID}-readBlocklist`,
|
||||
],
|
||||
id: 'blocklist_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeBlocklist', 'readBlocklist'],
|
||||
},
|
||||
{
|
||||
api: ['lists-read', 'lists-summary', `${APP_ID}-readBlocklist`],
|
||||
id: 'blocklist_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readBlocklist'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const eventFiltersSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Event Filters access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.eventFilters', {
|
||||
defaultMessage: 'Event Filters',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Filter out endpoint events that you do not need or want stored in Elasticsearch.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [
|
||||
'lists-all',
|
||||
'lists-read',
|
||||
'lists-summary',
|
||||
`${APP_ID}-writeEventFilters`,
|
||||
`${APP_ID}-readEventFilters`,
|
||||
],
|
||||
id: 'event_filters_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeEventFilters', 'readEventFilters'],
|
||||
},
|
||||
{
|
||||
api: ['lists-read', 'lists-summary', `${APP_ID}-readEventFilters`],
|
||||
id: 'event_filters_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readEventFilters'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const policyManagementSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Policy Management access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.policyManagement', {
|
||||
defaultMessage: 'Elastic Defend Policy Management',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writePolicyManagement`, `${APP_ID}-readPolicyManagement`],
|
||||
id: 'policy_management_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writePolicyManagement', 'readPolicyManagement'],
|
||||
},
|
||||
{
|
||||
api: [`${APP_ID}-readPolicyManagement`],
|
||||
id: 'policy_management_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readPolicyManagement'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const responseActionsHistorySubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Response Actions History access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory',
|
||||
{
|
||||
defaultMessage: 'Response Actions History',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.description',
|
||||
{
|
||||
defaultMessage: 'Access the history of response actions performed on endpoints.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeActionsLogManagement`, `${APP_ID}-readActionsLogManagement`],
|
||||
id: 'actions_log_management_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeActionsLogManagement', 'readActionsLogManagement'],
|
||||
},
|
||||
{
|
||||
api: [`${APP_ID}-readActionsLogManagement`],
|
||||
id: 'actions_log_management_read',
|
||||
includeIn: 'none',
|
||||
name: 'Read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readActionsLogManagement'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const hostIsolationSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Host Isolation access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.hostIsolation', {
|
||||
defaultMessage: 'Host Isolation',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.description',
|
||||
{ defaultMessage: 'Perform the "isolate" and "release" response actions.' }
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeHostIsolation`],
|
||||
id: 'host_isolation_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeHostIsolation'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const processOperationsSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.processOperations.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Process Operations access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.processOperations', {
|
||||
defaultMessage: 'Process Operations',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.processOperations.description',
|
||||
{
|
||||
defaultMessage: 'Perform process-related response actions in the response console.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeProcessOperations`],
|
||||
id: 'process_operations_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeProcessOperations'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const fileOperationsSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.fileOperations.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for File Operations access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.fileOperations', {
|
||||
defaultMessage: 'File Operations',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.fileOperations.description',
|
||||
{
|
||||
defaultMessage: 'Perform file-related response actions in the response console.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeFileOperations`],
|
||||
id: 'file_operations_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeFileOperations'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// execute operations are not available in 8.7,
|
||||
// but will be available in 8.8
|
||||
export const executeActionSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.executeOperations.privilegesTooltip',
|
||||
{
|
||||
defaultMessage: 'All Spaces is required for Execute Operations access.',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.executeOperations', {
|
||||
defaultMessage: 'Execute Operations',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.executeOperations.description',
|
||||
{
|
||||
// TODO: Update this description before 8.8 FF
|
||||
defaultMessage: 'Perform script execution on the endpoint.',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`${APP_ID}-writeExecuteOperations`],
|
||||
id: 'execute_operations_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeExecuteOperations'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 type { KibanaFeatureConfig, SubFeaturePrivilegeConfig } from '@kbn/features-plugin/common';
|
||||
import type { AppFeatureKey } from '../../../common';
|
||||
import type { AppFeatureSecurityKey, AppFeatureCasesKey } from '../../../common/types/app_features';
|
||||
import type { RecursivePartial } from '../../../common/utility_types';
|
||||
|
||||
export type SubFeaturesPrivileges = RecursivePartial<SubFeaturePrivilegeConfig>;
|
||||
export type AppFeatureKibanaConfig = RecursivePartial<KibanaFeatureConfig> & {
|
||||
subFeaturesPrivileges?: SubFeaturesPrivileges[];
|
||||
};
|
||||
export type AppFeaturesConfig = Record<AppFeatureKey, AppFeatureKibanaConfig>;
|
||||
export type AppFeaturesSecurityConfig = Record<AppFeatureSecurityKey, AppFeatureKibanaConfig>;
|
||||
export type AppFeaturesCasesConfig = Record<AppFeatureCasesKey, AppFeatureKibanaConfig>;
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { RISK_SCORE_INDEX_STATUS_API_URL } from '../../../../common/constants';
|
||||
import { APP_ID, RISK_SCORE_INDEX_STATUS_API_URL } from '../../../../common/constants';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
|
||||
import { buildSiemResponse } from '../../detection_engine/routes/utils';
|
||||
|
@ -21,7 +21,7 @@ export const getRiskScoreIndexStatusRoute = (router: SecuritySolutionPluginRoute
|
|||
query: buildRouteValidation(indexStatusSchema),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
|
||||
import { INTERNAL_RISK_SCORE_URL } from '../../../../../common/constants';
|
||||
import { APP_ID, INTERNAL_RISK_SCORE_URL } from '../../../../../common/constants';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../../types';
|
||||
|
||||
import type { SetupPlugins } from '../../../../plugin';
|
||||
|
@ -28,7 +28,7 @@ export const installRiskScoresRoute = (
|
|||
path: INTERNAL_RISK_SCORE_URL,
|
||||
validate: onboardingRiskScoreSchema,
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
|
|
|
@ -7,20 +7,10 @@
|
|||
|
||||
import type { Observable } from 'rxjs';
|
||||
import LRU from 'lru-cache';
|
||||
import {
|
||||
QUERY_RULE_TYPE_ID,
|
||||
INDICATOR_RULE_TYPE_ID,
|
||||
ML_RULE_TYPE_ID,
|
||||
EQL_RULE_TYPE_ID,
|
||||
SAVED_QUERY_RULE_TYPE_ID,
|
||||
THRESHOLD_RULE_TYPE_ID,
|
||||
NEW_TERMS_RULE_TYPE_ID,
|
||||
} from '@kbn/securitysolution-rules';
|
||||
|
||||
import { QUERY_RULE_TYPE_ID, SAVED_QUERY_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import { SavedObjectsClient } from '@kbn/core/server';
|
||||
import type { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
||||
|
||||
import { ECS_COMPONENT_TEMPLATE_NAME } from '@kbn/alerting-plugin/server';
|
||||
import { mappingFromFieldMap } from '@kbn/alerting-plugin/common';
|
||||
import type { IRuleDataClient } from '@kbn/rule-registry-plugin/server';
|
||||
|
@ -47,12 +37,7 @@ import { AppClientFactory } from './client';
|
|||
import type { ConfigType } from './config';
|
||||
import { createConfig } from './config';
|
||||
import { initUiSettings } from './ui_settings';
|
||||
import {
|
||||
APP_ID,
|
||||
SERVER_APP_ID,
|
||||
LEGACY_NOTIFICATIONS_ID,
|
||||
DEFAULT_ALERTS_INDEX,
|
||||
} from '../common/constants';
|
||||
import { APP_ID, SERVER_APP_ID, DEFAULT_ALERTS_INDEX } from '../common/constants';
|
||||
import { registerEndpointRoutes } from './endpoint/routes/metadata';
|
||||
import { registerPolicyRoutes } from './endpoint/routes/policy';
|
||||
import { registerActionRoutes } from './endpoint/routes/actions';
|
||||
|
@ -71,7 +56,6 @@ import { licenseService } from './lib/license';
|
|||
import { PolicyWatcher } from './endpoint/lib/policy/license_watch';
|
||||
import previewPolicy from './lib/detection_engine/routes/index/preview_policy.json';
|
||||
import { createRuleExecutionLogService } from './lib/detection_engine/rule_monitoring';
|
||||
import { getKibanaPrivilegesFeaturePrivileges, getCasesKibanaFeature } from './features';
|
||||
import { EndpointMetadataService } from './endpoint/services/metadata';
|
||||
import type {
|
||||
CreateRuleOptions,
|
||||
|
@ -106,6 +90,7 @@ import { setIsElasticCloudDeployment } from './lib/telemetry/helpers';
|
|||
import { artifactService } from './lib/telemetry/artifact';
|
||||
import { endpointFieldsProvider } from './search_strategy/endpoint_fields';
|
||||
import { ENDPOINT_FIELDS_SEARCH_STRATEGY } from '../common/endpoint/constants';
|
||||
import { AppFeatures } from './lib/app_features';
|
||||
|
||||
export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract';
|
||||
|
||||
|
@ -114,6 +99,7 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
private readonly config: ConfigType;
|
||||
private readonly logger: Logger;
|
||||
private readonly appClientFactory: AppClientFactory;
|
||||
private readonly appFeatures: AppFeatures;
|
||||
|
||||
private readonly endpointAppContextService = new EndpointAppContextService();
|
||||
private readonly telemetryReceiver: ITelemetryReceiver;
|
||||
|
@ -136,6 +122,7 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
this.config = serverConfig;
|
||||
this.logger = context.logger.get();
|
||||
this.appClientFactory = new AppClientFactory();
|
||||
this.appFeatures = new AppFeatures(this.logger, this.config.experimentalFeatures);
|
||||
|
||||
// Cache up to three artifacts with a max retention of 5 mins each
|
||||
this.artifactsCache = new LRU<string, Buffer>({ max: 3, maxAge: 1000 * 60 * 5 });
|
||||
|
@ -160,11 +147,12 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
): SecuritySolutionPluginSetup {
|
||||
this.logger.debug('plugin setup');
|
||||
|
||||
const { appClientFactory, pluginContext, config, logger } = this;
|
||||
const { appClientFactory, appFeatures, pluginContext, config, logger } = this;
|
||||
const experimentalFeatures = config.experimentalFeatures;
|
||||
|
||||
initSavedObjects(core.savedObjects);
|
||||
initUiSettings(core.uiSettings, experimentalFeatures);
|
||||
appFeatures.init(plugins.features);
|
||||
|
||||
const ruleExecutionLogService = createRuleExecutionLogService(config, logger, core, plugins);
|
||||
ruleExecutionLogService.registerEventLogProvider();
|
||||
|
@ -318,22 +306,6 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
plugins.encryptedSavedObjects?.canEncrypt === true
|
||||
);
|
||||
|
||||
const ruleTypes = [
|
||||
LEGACY_NOTIFICATIONS_ID,
|
||||
EQL_RULE_TYPE_ID,
|
||||
INDICATOR_RULE_TYPE_ID,
|
||||
ML_RULE_TYPE_ID,
|
||||
QUERY_RULE_TYPE_ID,
|
||||
SAVED_QUERY_RULE_TYPE_ID,
|
||||
THRESHOLD_RULE_TYPE_ID,
|
||||
NEW_TERMS_RULE_TYPE_ID,
|
||||
];
|
||||
|
||||
plugins.features.registerKibanaFeature(
|
||||
getKibanaPrivilegesFeaturePrivileges(ruleTypes, experimentalFeatures)
|
||||
);
|
||||
plugins.features.registerKibanaFeature(getCasesKibanaFeature());
|
||||
|
||||
if (plugins.alerting != null) {
|
||||
const ruleNotificationType = legacyRulesNotificationAlertType({ logger });
|
||||
|
||||
|
@ -408,7 +380,9 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
*/
|
||||
plugins.guidedOnboarding.registerGuideConfig(siemGuideId, siemGuideConfig);
|
||||
|
||||
return {};
|
||||
return {
|
||||
setAppFeatures: this.appFeatures.set.bind(this.appFeatures),
|
||||
};
|
||||
}
|
||||
|
||||
public start(
|
||||
|
|
|
@ -41,6 +41,7 @@ import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/
|
|||
import type { SharePluginStart } from '@kbn/share-plugin/server';
|
||||
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
|
||||
import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server';
|
||||
import type { AppFeatures } from './lib/app_features/app_features';
|
||||
|
||||
export interface SecuritySolutionPluginSetupDependencies {
|
||||
alerting: AlertingPluginSetup;
|
||||
|
@ -81,8 +82,12 @@ export interface SecuritySolutionPluginStartDependencies {
|
|||
share: SharePluginStart;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SecuritySolutionPluginSetup {}
|
||||
export interface SecuritySolutionPluginSetup {
|
||||
/**
|
||||
* Sets the app features that are available to the Security Solution
|
||||
*/
|
||||
setAppFeatures: AppFeatures['set'];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SecuritySolutionPluginStart {}
|
||||
|
|
19
x-pack/plugins/serverless_security/common/config.ts
Normal file
19
x-pack/plugins/serverless_security/common/config.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const productLineId = schema.oneOf([
|
||||
schema.literal('securityEssentials'),
|
||||
schema.literal('securityComplete'),
|
||||
]);
|
||||
export type SecurityProductLineId = TypeOf<typeof productLineId>;
|
||||
|
||||
export const productLineIds = schema.arrayOf<SecurityProductLineId>(productLineId, {
|
||||
defaultValue: ['securityEssentials'],
|
||||
});
|
||||
export type SecurityProductLineIds = TypeOf<typeof productLineIds>;
|
26
x-pack/plugins/serverless_security/common/jest.config.js
Normal file
26
x-pack/plugins/serverless_security/common/jest.config.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/serverless_security/common'],
|
||||
testMatch: ['<rootDir>/x-pack/plugins/serverless_security/common/**/*.test.{js,mjs,ts,tsx}'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/serverless_security/common',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/serverless_security/common/**/*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/common/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/common/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/common/*mock*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/common/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/common/*.d.ts',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/common/*.config.ts',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/common/index.{js,ts,tsx}',
|
||||
],
|
||||
};
|
14
x-pack/plugins/serverless_security/common/pli/pli_config.ts
Normal file
14
x-pack/plugins/serverless_security/common/pli/pli_config.ts
Normal file
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import type { SecurityProductLineId } from '../config';
|
||||
|
||||
export const PLI_APP_FEATURES: Record<SecurityProductLineId, readonly AppFeatureKey[]> = {
|
||||
securityEssentials: [],
|
||||
securityComplete: [AppFeatureKey.advancedInsights, AppFeatureKey.casesConnectors],
|
||||
} as const;
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { getProductAppFeatures } from './pli_features';
|
||||
import * as pliConfig from './pli_config';
|
||||
|
||||
describe('getProductAppFeatures', () => {
|
||||
it('returns the union of all enabled PLIs features', () => {
|
||||
// @ts-ignore reassigning readonly value for testing
|
||||
pliConfig.PLI_APP_FEATURES = { securityEssentials: ['foo'], securityComplete: ['baz'] };
|
||||
|
||||
expect(getProductAppFeatures(['securityEssentials', 'securityComplete'])).toEqual({
|
||||
foo: true,
|
||||
baz: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a single PLI when only one is enabled', () => {
|
||||
// @ts-ignore reassigning readonly value for testing
|
||||
pliConfig.PLI_APP_FEATURES = { securityEssentials: [], securityComplete: ['foo'] };
|
||||
expect(getProductAppFeatures(['securityEssentials', 'securityComplete'])).toEqual({
|
||||
foo: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty object if no PLIs are enabled', () => {
|
||||
expect(getProductAppFeatures([])).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 type { AppFeatureKeys } from '@kbn/security-solution-plugin/common';
|
||||
import { SecurityProductLineId } from '../config';
|
||||
import { PLI_APP_FEATURES } from './pli_config';
|
||||
|
||||
/**
|
||||
* Returns the U (union) of all enabled PLIs features in a single object.
|
||||
*/
|
||||
export const getProductAppFeatures = (productLineIds: SecurityProductLineId[]): AppFeatureKeys =>
|
||||
productLineIds.reduce<AppFeatureKeys>((appFeatures, productLineId) => {
|
||||
const productAppFeatures = PLI_APP_FEATURES[productLineId];
|
||||
|
||||
productAppFeatures.forEach((featureName) => {
|
||||
appFeatures[featureName] = true;
|
||||
});
|
||||
|
||||
return appFeatures;
|
||||
}, {} as AppFeatureKeys);
|
|
@ -18,7 +18,9 @@
|
|||
"securitySolution",
|
||||
"kibanaReact"
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
"optionalPlugins": [
|
||||
"essSecurity"
|
||||
],
|
||||
"requiredBundles": []
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { registerUpsellings } from './register_upsellings';
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
|
||||
import { SecurityProductLineIds } from '../../../../common/config';
|
||||
|
||||
export const GenericUpsellingPage: React.FC<{ projectPLIs: SecurityProductLineIds }> = React.memo(
|
||||
({ projectPLIs }) => {
|
||||
const upsellingPLI = projectPLIs.includes('securityComplete')
|
||||
? 'Security Complete'
|
||||
: 'Security Essential';
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="logoSecurity"
|
||||
title={<>This is a testing component for a Serverless upselling prompt.</>}
|
||||
body={
|
||||
<>
|
||||
Get <EuiLink href="#">{upsellingPLI}</EuiLink> to enable this feature
|
||||
<br />
|
||||
<br />
|
||||
<iframe
|
||||
title="money"
|
||||
src="https://giphy.com/embed/px8O7NANzzaqk"
|
||||
width="480"
|
||||
height="283"
|
||||
frameBorder="0"
|
||||
className="giphy-embed"
|
||||
allowFullScreen
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { GenericUpsellingPage as default };
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
|
||||
import { SecurityProductLineIds } from '../../../../common/config';
|
||||
|
||||
export const GenericUpsellingSection: React.FC<{ projectPLIs: SecurityProductLineIds }> =
|
||||
React.memo(({ projectPLIs }) => {
|
||||
const upsellingPLI = projectPLIs.includes('securityComplete')
|
||||
? 'Security Complete'
|
||||
: 'Security Essential';
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="logoSecurity"
|
||||
title={<>This is a testing component for a Serverless upselling prompt.</>}
|
||||
body={
|
||||
<>
|
||||
Get <EuiLink href="#">{upsellingPLI}</EuiLink> to enable this feature
|
||||
<br />
|
||||
<br />
|
||||
<iframe
|
||||
title="money"
|
||||
src="https://giphy.com/embed/px8O7NANzzaqk"
|
||||
width="480"
|
||||
height="283"
|
||||
frameBorder="0"
|
||||
className="giphy-embed"
|
||||
allowFullScreen
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { GenericUpsellingSection as default };
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { UpsellingService } from '@kbn/security-solution-plugin/public';
|
||||
import { registerUpsellings } from './register_upsellings';
|
||||
|
||||
const mockGetProductAppFeatures = jest.fn();
|
||||
jest.mock('../../../common/pli/pli_features', () => ({
|
||||
getProductAppFeatures: () => mockGetProductAppFeatures(),
|
||||
}));
|
||||
|
||||
describe('registerUpsellings', () => {
|
||||
it('registers entity analytics upsellings page and section when PLIs features are disabled', () => {
|
||||
mockGetProductAppFeatures.mockReturnValue({}); // return empty object to simulate no features enabled
|
||||
|
||||
const registerPages = jest.fn();
|
||||
const registerSections = jest.fn();
|
||||
const upselling = {
|
||||
registerPages,
|
||||
registerSections,
|
||||
} as unknown as UpsellingService;
|
||||
|
||||
registerUpsellings(upselling, ['securityEssentials', 'securityComplete']);
|
||||
|
||||
expect(registerPages).toHaveBeenCalledTimes(1);
|
||||
expect(registerPages).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
['entity-analytics']: expect.any(Function),
|
||||
})
|
||||
);
|
||||
|
||||
expect(registerSections).toHaveBeenCalledTimes(1);
|
||||
expect(registerSections).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entity_analytics_panel: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 React, { lazy } from 'react';
|
||||
import type { UpsellingService } from '@kbn/security-solution-plugin/public';
|
||||
import { SecurityPageName, AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import type {
|
||||
PageUpsellings,
|
||||
SectionUpsellings,
|
||||
UpsellingSectionId,
|
||||
} from '@kbn/security-solution-plugin/public';
|
||||
|
||||
import type { SecurityProductLineIds } from '../../../common/config';
|
||||
import { getProductAppFeatures } from '../../../common/pli/pli_features';
|
||||
|
||||
const GenericUpsellingPageLazy = lazy(() => import('./pages/generic_upselling_page'));
|
||||
const GenericUpsellingSectionLazy = lazy(() => import('./pages/generic_upselling_section'));
|
||||
|
||||
interface UpsellingsConfig {
|
||||
feature: AppFeatureKey;
|
||||
component: React.ComponentType;
|
||||
}
|
||||
|
||||
type UpsellingPages = Array<UpsellingsConfig & { pageName: SecurityPageName }>;
|
||||
type UpsellingSections = Array<UpsellingsConfig & { id: UpsellingSectionId }>;
|
||||
|
||||
export const registerUpsellings = (
|
||||
upselling: UpsellingService,
|
||||
projectPLIs: SecurityProductLineIds
|
||||
) => {
|
||||
const PLIsFeatures = getProductAppFeatures(projectPLIs);
|
||||
|
||||
const upsellingPages = getUpsellingPages(projectPLIs).reduce<PageUpsellings>(
|
||||
(pageUpsellings, { pageName, feature, component }) => {
|
||||
if (!PLIsFeatures[feature]) {
|
||||
pageUpsellings[pageName] = component;
|
||||
}
|
||||
return pageUpsellings;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const upsellingSections = getUpsellingSections(projectPLIs).reduce<SectionUpsellings>(
|
||||
(sectionUpsellings, { id, feature, component }) => {
|
||||
if (!PLIsFeatures[feature]) {
|
||||
sectionUpsellings[id] = component;
|
||||
}
|
||||
return sectionUpsellings;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
upselling.registerPages(upsellingPages);
|
||||
upselling.registerSections(upsellingSections);
|
||||
};
|
||||
|
||||
// Upselling configuration for pages and sections components
|
||||
const getUpsellingPages = (projectPLIs: SecurityProductLineIds): UpsellingPages => [
|
||||
{
|
||||
pageName: SecurityPageName.entityAnalytics,
|
||||
feature: AppFeatureKey.advancedInsights,
|
||||
component: () => <GenericUpsellingPageLazy projectPLIs={projectPLIs} />,
|
||||
},
|
||||
];
|
||||
|
||||
const getUpsellingSections = (projectPLIs: SecurityProductLineIds): UpsellingSections => [
|
||||
{
|
||||
id: 'entity_analytics_panel',
|
||||
feature: AppFeatureKey.advancedInsights,
|
||||
component: () => <GenericUpsellingSectionLazy projectPLIs={projectPLIs} />,
|
||||
},
|
||||
];
|
|
@ -5,12 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { ServerlessSecurityPlugin } from './plugin';
|
||||
|
||||
// This exports static code and TypeScript types,
|
||||
// as well as, Kibana Platform `plugin()` initializer.
|
||||
export function plugin() {
|
||||
return new ServerlessSecurityPlugin();
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new ServerlessSecurityPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export type { ServerlessSecurityPluginSetup, ServerlessSecurityPluginStart } from './types';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
import { getSecurityGetStartedComponent } from './components/get_started';
|
||||
import { getSecuritySideNavComponent } from './components/side_navigation';
|
||||
import {
|
||||
|
@ -13,7 +13,9 @@ import {
|
|||
ServerlessSecurityPluginStart,
|
||||
ServerlessSecurityPluginSetupDependencies,
|
||||
ServerlessSecurityPluginStartDependencies,
|
||||
ServerlessSecurityPublicConfig,
|
||||
} from './types';
|
||||
import { registerUpsellings } from './components/upselling';
|
||||
|
||||
export class ServerlessSecurityPlugin
|
||||
implements
|
||||
|
@ -24,10 +26,17 @@ export class ServerlessSecurityPlugin
|
|||
ServerlessSecurityPluginStartDependencies
|
||||
>
|
||||
{
|
||||
private config: ServerlessSecurityPublicConfig;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<ServerlessSecurityPublicConfig>();
|
||||
}
|
||||
|
||||
public setup(
|
||||
_core: CoreSetup,
|
||||
_setupDeps: ServerlessSecurityPluginSetupDependencies
|
||||
setupDeps: ServerlessSecurityPluginSetupDependencies
|
||||
): ServerlessSecurityPluginSetup {
|
||||
registerUpsellings(setupDeps.securitySolution.upselling, this.config.productLineIds);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import {
|
||||
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import type {
|
||||
PluginSetup as SecuritySolutionPluginSetup,
|
||||
PluginStart as SecuritySolutionPluginStart,
|
||||
} from '@kbn/security-solution-plugin/public';
|
||||
import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import type { SecurityProductLineIds } from '../common/config';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ServerlessSecurityPluginSetup {}
|
||||
|
@ -29,3 +30,7 @@ export interface ServerlessSecurityPluginStartDependencies {
|
|||
securitySolution: SecuritySolutionPluginStart;
|
||||
serverless: ServerlessPluginStart;
|
||||
}
|
||||
|
||||
export interface ServerlessSecurityPublicConfig {
|
||||
productLineIds: SecurityProductLineIds;
|
||||
}
|
||||
|
|
|
@ -7,17 +7,17 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
import { productLineIds } from '../common/config';
|
||||
|
||||
export * from './types';
|
||||
|
||||
const configSchema = schema.object({
|
||||
export const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
productLineIds,
|
||||
});
|
||||
export type ServerlessSecurityConfig = TypeOf<typeof configSchema>;
|
||||
|
||||
type ConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<ConfigType> = {
|
||||
export const config: PluginConfigDescriptor<ServerlessSecurityConfig> = {
|
||||
exposeToBrowser: {
|
||||
productLineIds: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
||||
export type ServerlessSecurityConfig = TypeOf<typeof configSchema>;
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext, Plugin } from '@kbn/core/server';
|
||||
import { PluginInitializerContext, Plugin, CoreSetup } from '@kbn/core/server';
|
||||
import { ServerlessSecurityConfig } from './config';
|
||||
import { getProductAppFeatures } from '../common/pli/pli_features';
|
||||
|
||||
import {
|
||||
ServerlessSecurityPluginSetup,
|
||||
|
@ -23,9 +25,24 @@ export class ServerlessSecurityPlugin
|
|||
ServerlessSecurityPluginStartDependencies
|
||||
>
|
||||
{
|
||||
constructor(_initializerContext: PluginInitializerContext) {}
|
||||
private config: ServerlessSecurityConfig;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<ServerlessSecurityConfig>();
|
||||
}
|
||||
|
||||
public setup(_coreSetup: CoreSetup, pluginsSetup: ServerlessSecurityPluginSetupDependencies) {
|
||||
// essSecurity plugin should always be disabled when serverlessSecurity is enabled.
|
||||
// This check is an additional layer of security to prevent double registrations when
|
||||
// `plugins.forceEnableAllPlugins` flag is enabled).
|
||||
const shouldRegister = pluginsSetup.essSecurity == null;
|
||||
|
||||
if (shouldRegister) {
|
||||
pluginsSetup.securitySolution.setAppFeatures(
|
||||
getProductAppFeatures(this.config.productLineIds)
|
||||
);
|
||||
}
|
||||
|
||||
public setup() {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import type { PluginSetupContract, PluginStartContract } from '@kbn/features-plugin/server';
|
||||
import {
|
||||
PluginSetup as SecuritySolutionPluginSetup,
|
||||
PluginStart as SecuritySolutionPluginStart,
|
||||
} from '@kbn/security-solution-plugin/server';
|
||||
|
||||
import type { EssSecurityPluginSetup } from '@kbn/ess-security/server';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ServerlessSecurityPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
@ -19,9 +22,12 @@ export interface ServerlessSecurityPluginStart {}
|
|||
export interface ServerlessSecurityPluginSetupDependencies {
|
||||
security: SecurityPluginSetup;
|
||||
securitySolution: SecuritySolutionPluginSetup;
|
||||
features: PluginSetupContract;
|
||||
essSecurity: EssSecurityPluginSetup;
|
||||
}
|
||||
|
||||
export interface ServerlessSecurityPluginStartDependencies {
|
||||
security: SecurityPluginStart;
|
||||
securitySolution: SecuritySolutionPluginStart;
|
||||
features: PluginStartContract;
|
||||
}
|
||||
|
|
|
@ -27,5 +27,7 @@
|
|||
"@kbn/i18n-react",
|
||||
"@kbn/i18n",
|
||||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/features-plugin",
|
||||
"@kbn/ess-security",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -4103,6 +4103,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/ess-security@link:x-pack/plugins/ess_security":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/event-annotation-plugin@link:src/plugins/event_annotation":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue