diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts index 615eb3f05876e..b7f96bfdafdad 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export * from './installed_integrations'; export * from './rule_monitoring'; export * from './rule_params'; export * from './schemas'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/installed_integrations.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/installed_integrations.ts new file mode 100644 index 0000000000000..4eb06b2c3dfd4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/installed_integrations.ts @@ -0,0 +1,241 @@ +/* + * 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. + */ + +// ------------------------------------------------------------------------------------------------- +// Installed package + +/** + * Basic information about an installed Fleet package. + */ +export interface InstalledPackageBasicInfo { + /** + * Name is a unique package id within a given cluster. + * There can't be 2 or more different packages with the same name. + * @example 'aws' + */ + package_name: string; + + /** + * Title is a user-friendly name of the package that we show in the UI. + * @example 'AWS' + */ + package_title: string; + + /** + * Version of the package. Semver-compatible. + * @example '1.2.3' + */ + package_version: string; +} + +/** + * Information about an installed Fleet package including its integrations. + * + * @example + * { + * package_name: 'aws', + * package_title: 'AWS', + * package_version: '1.16.1', + * integrations: [ + * { + * integration_name: 'billing', + * integration_title: 'AWS Billing', + * is_enabled: false + * }, + * { + * integration_name: 'cloudtrail', + * integration_title: 'AWS CloudTrail', + * is_enabled: false + * }, + * { + * integration_name: 'cloudwatch', + * integration_title: 'AWS CloudWatch', + * is_enabled: false + * }, + * { + * integration_name: 'cloudfront', + * integration_title: 'Amazon CloudFront', + * is_enabled: true + * } + * ] + * } + */ +export interface InstalledPackage extends InstalledPackageBasicInfo { + integrations: InstalledIntegrationBasicInfo[]; +} + +// ------------------------------------------------------------------------------------------------- +// Installed integration + +/** + * Basic information about an installed Fleet integration. + * An integration belongs to a package. A package can contain one or many integrations. + */ +export interface InstalledIntegrationBasicInfo { + /** + * Name identifies an integration within its package. + * @example 'cloudtrail' + */ + integration_name: string; + + /** + * Title is a user-friendly name of the integration that we show in the UI. + * @example 'AWS CloudTrail' + */ + integration_title: string; + + /** + * Whether this integration is enabled or not in at least one package policy in Fleet. + */ + is_enabled: boolean; +} + +/** + * Information about an installed Fleet integration including info about its package. + * + * @example + * { + * package_name: 'aws', + * package_title: 'AWS', + * package_version: '1.16.1', + * integration_name: 'cloudtrail', + * integration_title: 'AWS CloudTrail', + * is_enabled: false + * } + * + * @example + * { + * package_name: 'system', + * package_title: 'System', + * package_version: '1.13.0', + * is_enabled: true + * } + */ +export interface InstalledIntegration extends InstalledPackageBasicInfo { + /** + * Name identifies an integration within its package. + * Undefined when package name === integration name. This indicates that it's the only integration + * within this package. + * @example 'cloudtrail' + * @example undefined + */ + integration_name?: string; + + /** + * Title is a user-friendly name of the integration that we show in the UI. + * Undefined when package name === integration name. This indicates that it's the only integration + * within this package. + * @example 'AWS CloudTrail' + * @example undefined + */ + integration_title?: string; + + /** + * Whether this integration is enabled or not in at least one package policy in Fleet. + */ + is_enabled: boolean; +} + +// ------------------------------------------------------------------------------------------------- +// Arrays of installed packages and integrations + +/** + * An array of installed packages with their integrations. + * This is a hierarchical way of representing installed integrations. + * + * @example + * [ + * { + * package_name: 'aws', + * package_title: 'AWS', + * package_version: '1.16.1', + * integrations: [ + * { + * integration_name: 'billing', + * integration_title: 'AWS Billing', + * is_enabled: false + * }, + * { + * integration_name: 'cloudtrail', + * integration_title: 'AWS CloudTrail', + * is_enabled: false + * }, + * { + * integration_name: 'cloudwatch', + * integration_title: 'AWS CloudWatch', + * is_enabled: false + * }, + * { + * integration_name: 'cloudfront', + * integration_title: 'Amazon CloudFront', + * is_enabled: true + * } + * ] + * }, + * { + * package_name: 'system', + * package_title: 'System', + * package_version: '1.13.0', + * integrations: [ + * { + * integration_name: 'system', + * integration_title: 'System logs and metrics', + * is_enabled: true + * } + * ] + * } + * ] + */ +export type InstalledPackageArray = InstalledPackage[]; + +/** + * An array of installed integrations with info about their packages. + * This is a flattened way of representing installed integrations. + * + * @example + * [ + * { + * package_name: 'aws', + * package_title: 'AWS', + * package_version: '1.16.1', + * integration_name: 'billing', + * integration_title: 'AWS Billing', + * is_enabled: false + * }, + * { + * package_name: 'aws', + * package_title: 'AWS', + * package_version: '1.16.1', + * integration_name: 'cloudtrail', + * integration_title: 'AWS CloudTrail', + * is_enabled: false + * }, + * { + * package_name: 'aws', + * package_title: 'AWS', + * package_version: '1.16.1', + * integration_name: 'cloudwatch', + * integration_title: 'AWS CloudWatch', + * is_enabled: false + * }, + * { + * package_name: 'aws', + * package_title: 'AWS', + * package_version: '1.16.1', + * integration_name: 'cloudfront', + * integration_title: 'Amazon CloudFront', + * is_enabled: true + * }, + * { + * package_name: 'system', + * package_title: 'System', + * package_version: '1.13.0', + * is_enabled: true + * } + * ] + */ +export type InstalledIntegrationArray = InstalledIntegration[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_installed_integrations_response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_installed_integrations_response_schema.ts new file mode 100644 index 0000000000000..b21a8d1bbd50d --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_installed_integrations_response_schema.ts @@ -0,0 +1,12 @@ +/* + * 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 { InstalledIntegrationArray } from '../common'; + +export interface GetInstalledIntegrationsResponse { + installed_integrations: InstalledIntegrationArray; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 60e8f0c8cf858..085e3bc8b00ce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -7,6 +7,7 @@ import type { MockedKeys } from '@kbn/utility-types/jest'; import type { AwaitedProperties } from '@kbn/utility-types'; +import type { KibanaRequest } from '@kbn/core/server'; import { coreMock } from '@kbn/core/server/mocks'; import { ActionsApiRequestHandlerContext } from '@kbn/actions-plugin/server'; @@ -31,6 +32,7 @@ import type { SecuritySolutionApiRequestHandlerContext, SecuritySolutionRequestHandlerContext, } from '../../../../types'; + import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz'; import { EndpointAuthz } from '../../../../../common/endpoint/types/authz'; @@ -125,6 +127,14 @@ const createSecuritySolutionRequestContextMock = ( getRuleDataService: jest.fn(() => clients.ruleDataService), getRuleExecutionLog: jest.fn(() => clients.ruleExecutionLog), getExceptionListClient: jest.fn(() => clients.lists.exceptionListClient), + getInternalFleetServices: jest.fn(() => { + // TODO: Mock EndpointInternalFleetServicesInterface and return the mocked object. + throw new Error('Not implemented'); + }), + getScopedFleetServices: jest.fn((req: KibanaRequest) => { + // TODO: Mock EndpointScopedFleetServicesInterface and return the mocked object. + throw new Error('Not implemented'); + }), }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/fleet/get_installed_integrations/get_installed_integrations_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/fleet/get_installed_integrations/get_installed_integrations_route.ts new file mode 100644 index 0000000000000..3682949bd652d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/fleet/get_installed_integrations/get_installed_integrations_route.ts @@ -0,0 +1,72 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; + +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL } from '../../../../../../common/constants'; +import { GetInstalledIntegrationsResponse } from '../../../../../../common/detection_engine/schemas/response/get_installed_integrations_response_schema'; +import { buildSiemResponse } from '../../utils'; +import { createInstalledIntegrationSet } from './installed_integration_set'; + +/** + * Returns an array of installed Fleet integrations and their packages. + */ +export const getInstalledIntegrationsRoute = (router: SecuritySolutionPluginRouter) => { + router.get( + { + path: DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL, + validate: {}, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['core', 'securitySolution']); + const fleet = ctx.securitySolution.getInternalFleetServices(); + const soClient = ctx.core.savedObjects.client; + const set = createInstalledIntegrationSet(); + + const packagePolicies = await fleet.packagePolicy.list(soClient, {}); + + packagePolicies.items.forEach((policy) => { + set.addPackagePolicy(policy); + }); + + const registryPackages = await Promise.all( + set.getPackages().map((packageInfo) => { + return fleet.packages.getRegistryPackage( + packageInfo.package_name, + packageInfo.package_version + ); + }) + ); + + registryPackages.forEach((registryPackage) => { + set.addRegistryPackage(registryPackage.packageInfo); + }); + + const installedIntegrations = set.getIntegrations(); + + const body: GetInstalledIntegrationsResponse = { + installed_integrations: installedIntegrations, + }; + + return response.ok({ body }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/fleet/get_installed_integrations/installed_integration_set.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/fleet/get_installed_integrations/installed_integration_set.ts new file mode 100644 index 0000000000000..5ea546359c972 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/fleet/get_installed_integrations/installed_integration_set.ts @@ -0,0 +1,145 @@ +/* + * 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 { flatten } from 'lodash'; +import { PackagePolicy, RegistryPackage } from '@kbn/fleet-plugin/common'; +import { + InstalledIntegration, + InstalledIntegrationArray, + InstalledIntegrationBasicInfo, + InstalledPackage, + InstalledPackageArray, + InstalledPackageBasicInfo, +} from '../../../../../../common/detection_engine/schemas/common'; + +export interface IInstalledIntegrationSet { + addPackagePolicy(policy: PackagePolicy): void; + addRegistryPackage(registryPackage: RegistryPackage): void; + + getPackages(): InstalledPackageArray; + getIntegrations(): InstalledIntegrationArray; +} + +type PackageMap = Map; + +interface PackageInfo extends InstalledPackageBasicInfo { + integrations: Map; +} + +export const createInstalledIntegrationSet = (): IInstalledIntegrationSet => { + const packageMap: PackageMap = new Map([]); + + const addPackagePolicy = (policy: PackagePolicy): void => { + const packageInfo = getPackageInfoFromPolicy(policy); + const integrationsInfo = getIntegrationsInfoFromPolicy(policy); + const packageKey = `${packageInfo.package_name}:${packageInfo.package_version}`; + const existingPackageInfo = packageMap.get(packageKey); + + if (existingPackageInfo == null) { + const integrationsMap = new Map(); + integrationsInfo.forEach((integration) => { + addIntegrationToMap(integrationsMap, integration); + }); + + packageMap.set(packageKey, { + ...packageInfo, + integrations: integrationsMap, + }); + } else { + integrationsInfo.forEach((integration) => { + addIntegrationToMap(existingPackageInfo.integrations, integration); + }); + } + }; + + const addRegistryPackage = (registryPackage: RegistryPackage): void => { + const policyTemplates = registryPackage.policy_templates ?? []; + const packageKey = `${registryPackage.name}:${registryPackage.version}`; + const existingPackageInfo = packageMap.get(packageKey); + + if (existingPackageInfo != null) { + for (const integration of existingPackageInfo.integrations.values()) { + const policyTemplate = policyTemplates.find((t) => t.name === integration.integration_name); + if (policyTemplate != null) { + integration.integration_title = policyTemplate.title; + } + } + } + }; + + const getPackages = (): InstalledPackageArray => { + const packages = Array.from(packageMap.values()); + return packages.map((packageInfo): InstalledPackage => { + const integrations = Array.from(packageInfo.integrations.values()); + return { ...packageInfo, integrations }; + }); + }; + + const getIntegrations = (): InstalledIntegrationArray => { + const packages = Array.from(packageMap.values()); + return flatten( + packages.map((packageInfo): InstalledIntegrationArray => { + const integrations = Array.from(packageInfo.integrations.values()); + return integrations.map((integrationInfo): InstalledIntegration => { + return packageInfo.package_name === integrationInfo.integration_name + ? { + package_name: packageInfo.package_name, + package_title: packageInfo.package_title, + package_version: packageInfo.package_version, + is_enabled: integrationInfo.is_enabled, + } + : { + package_name: packageInfo.package_name, + package_title: packageInfo.package_title, + package_version: packageInfo.package_version, + integration_name: integrationInfo.integration_name, + integration_title: integrationInfo.integration_title, + is_enabled: integrationInfo.is_enabled, + }; + }); + }) + ); + }; + + return { + addPackagePolicy, + addRegistryPackage, + getPackages, + getIntegrations, + }; +}; + +const getPackageInfoFromPolicy = (policy: PackagePolicy): InstalledPackageBasicInfo => { + return { + package_name: normalizeString(policy.package?.name), + package_title: normalizeString(policy.package?.title), + package_version: normalizeString(policy.package?.version), + }; +}; + +const getIntegrationsInfoFromPolicy = (policy: PackagePolicy): InstalledIntegrationBasicInfo[] => { + return policy.inputs.map((input) => { + return { + integration_name: normalizeString(input.policy_template), + integration_title: '', // this gets initialized later in addRegistryPackage() + is_enabled: input.enabled, + }; + }); +}; + +const normalizeString = (raw: string | null | undefined): string => { + return (raw ?? '').trim(); +}; + +const addIntegrationToMap = ( + map: Map, + integration: InstalledIntegrationBasicInfo +): void => { + if (!map.has(integration.integration_name) || integration.is_enabled) { + map.set(integration.integration_name, integration); + } +}; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index cbf2a596e962d..0dc27e0151db8 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -147,7 +147,14 @@ export class Plugin implements ISecuritySolutionPlugin { const eventLogService = plugins.eventLog; registerEventLogProvider(eventLogService); - const requestContextFactory = new RequestContextFactory({ config, logger, core, plugins }); + const requestContextFactory = new RequestContextFactory({ + config, + logger, + core, + plugins, + endpointAppContextService: this.endpointAppContextService, + }); + const router = core.http.createRouter(); core.http.registerRouteHandlerContext( APP_ID, diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 9f6b950c46202..4d3a9bf32c736 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -30,6 +30,7 @@ import { getEndpointAuthzInitialState, } from '../common/endpoint/service/authz'; import { licenseService } from './lib/license'; +import { EndpointAppContextService } from './endpoint/endpoint_app_context_services'; export interface IRequestContextFactory { create( @@ -43,6 +44,7 @@ interface ConstructorOptions { logger: Logger; core: SecuritySolutionPluginCoreSetupDependencies; plugins: SecuritySolutionPluginSetupDependencies; + endpointAppContextService: EndpointAppContextService; } export class RequestContextFactory implements IRequestContextFactory { @@ -57,7 +59,7 @@ export class RequestContextFactory implements IRequestContextFactory { request: KibanaRequest ): Promise { const { options, appClientFactory } = this; - const { config, logger, core, plugins } = options; + const { config, logger, core, plugins, endpointAppContextService } = options; const { lists, ruleRegistry, security } = plugins; const [, startPlugins] = await core.getStartServices(); @@ -122,6 +124,12 @@ export class RequestContextFactory implements IRequestContextFactory { const username = security?.authc.getCurrentUser(request)?.username || 'elastic'; return lists.getExceptionListClient(coreContext.savedObjects.client, username); }, + + getInternalFleetServices: memoize(() => endpointAppContextService.getInternalFleetServices()), + + getScopedFleetServices: memoize((req: KibanaRequest) => + endpointAppContextService.getScopedFleetServices(req) + ), }; } } diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 8763f2d00669e..d64cbd397ecfa 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -69,6 +69,7 @@ import { legacyCreateLegacyNotificationRoute } from '../lib/detection_engine/rou import { createSourcererDataViewRoute, getSourcererDataViewRoute } from '../lib/sourcerer/routes'; import { ITelemetryReceiver } from '../lib/telemetry/receiver'; import { telemetryDetectionRulesPreviewRoute } from '../lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route'; +import { getInstalledIntegrationsRoute } from '../lib/detection_engine/routes/fleet/get_installed_integrations/get_installed_integrations_route'; export const initRoutes = ( router: SecuritySolutionPluginRouter, @@ -118,6 +119,8 @@ export const initRoutes = ( getRuleExecutionEventsRoute(router); + getInstalledIntegrationsRoute(router); + createTimelinesRoute(router, config, security); patchTimelinesRoute(router, config, security); importRulesRoute(router, config, ml); diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index b1bf51439a3f6..3f27cb1cb5926 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -9,6 +9,7 @@ import type { IRouter, CustomRequestHandlerContext, CoreRequestHandlerContext, + KibanaRequest, } from '@kbn/core/server'; import type { ActionsApiRequestHandlerContext } from '@kbn/actions-plugin/server'; import type { AlertingApiRequestHandlerContext } from '@kbn/alerting-plugin/server'; @@ -22,6 +23,10 @@ import { ConfigType } from './config'; import { IRuleExecutionLogForRoutes } from './lib/detection_engine/rule_execution_log'; import { FrameworkRequest } from './lib/framework'; import { EndpointAuthz } from '../common/endpoint/types/authz'; +import { + EndpointInternalFleetServicesInterface, + EndpointScopedFleetServicesInterface, +} from './endpoint/services/fleet'; export { AppClient }; @@ -35,6 +40,8 @@ export interface SecuritySolutionApiRequestHandlerContext { getRuleDataService: () => IRuleDataService; getRuleExecutionLog: () => IRuleExecutionLogForRoutes; getExceptionListClient: () => ExceptionListClient | null; + getInternalFleetServices: () => EndpointInternalFleetServicesInterface; + getScopedFleetServices: (req: KibanaRequest) => EndpointScopedFleetServicesInterface; } export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext<{