diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.mock.ts b/x-pack/plugins/fleet/server/services/epm/package_service.mock.ts index 806f295ec2c4e..69e1217b0493c 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.mock.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.mock.ts @@ -12,6 +12,7 @@ const createClientMock = (): jest.Mocked => ({ ensureInstalledPackage: jest.fn(), fetchFindLatestPackage: jest.fn(), getPackage: jest.fn(), + getPackages: jest.fn(), reinstallEsAssets: jest.fn(), }); diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.ts b/x-pack/plugins/fleet/server/services/epm/package_service.ts index dfc02c4f68c57..b0dd6e9dc38b4 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.ts @@ -14,7 +14,10 @@ import type { Logger, } from '@kbn/core/server'; +import type { PackageList } from '../../../common'; + import type { + CategoryId, EsAssetReference, InstallablePackage, Installation, @@ -28,7 +31,7 @@ import { FleetUnauthorizedError } from '../../errors'; import { installTransforms, isTransform } from './elasticsearch/transform/install'; import type { FetchFindLatestPackageOptions } from './registry'; import { fetchFindLatestPackageOrThrow, getPackage } from './registry'; -import { ensureInstalledPackage, getInstallation } from './packages'; +import { ensureInstalledPackage, getInstallation, getPackages } from './packages'; export type InstalledAssetType = EsAssetReference; @@ -56,6 +59,12 @@ export interface PackageClient { packageVersion: string ): Promise<{ packageInfo: ArchivePackage; paths: string[] }>; + getPackages(params?: { + excludeInstallStatus?: false; + category?: CategoryId; + prerelease?: false; + }): Promise; + reinstallEsAssets( packageInfo: InstallablePackage, assetPaths: string[] @@ -137,6 +146,21 @@ class PackageClientImpl implements PackageClient { return getPackage(packageName, packageVersion, options); } + public async getPackages(params?: { + excludeInstallStatus?: false; + category?: CategoryId; + prerelease?: false; + }) { + const { excludeInstallStatus, category, prerelease } = params || {}; + await this.#runPreflight(); + return getPackages({ + savedObjectsClient: this.internalSoClient, + excludeInstallStatus, + category, + prerelease, + }); + } + public async reinstallEsAssets( packageInfo: InstallablePackage, assetPaths: string[] diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts index f411b5e229590..663317f334751 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts @@ -117,7 +117,7 @@ describe('Related integrations', () => { const rule = { name: 'Related integrations rule', integrations: [ - { name: 'Amazon CloudFront', installed: true, enabled: true }, + { name: 'AWS Cloudfront', installed: true, enabled: true }, { name: 'AWS CloudTrail', installed: true, enabled: false }, { name: 'Aws Unknown', installed: false, enabled: false }, { name: 'System', installed: true, enabled: true }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/installed_integration_set.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/installed_integration_set.ts index af35f7881bd00..dcf9d006c4315 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/installed_integration_set.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/installed_integration_set.ts @@ -5,8 +5,8 @@ * 2.0. */ +import type { PackageListItem, PackagePolicy } from '@kbn/fleet-plugin/common'; import { capitalize, flatten } from 'lodash'; -import type { PackagePolicy, ArchivePackage } from '@kbn/fleet-plugin/common'; import type { InstalledIntegration, InstalledIntegrationArray, @@ -17,8 +17,8 @@ import type { } from '../../../../../../common/detection_engine/fleet_integrations'; export interface IInstalledIntegrationSet { + addPackage(fleetPackage: PackageListItem): void; addPackagePolicy(policy: PackagePolicy): void; - addRegistryPackage(registryPackage: ArchivePackage): void; getPackages(): InstalledPackageArray; getIntegrations(): InstalledIntegrationArray; @@ -33,10 +33,57 @@ interface PackageInfo extends InstalledPackageBasicInfo { export const createInstalledIntegrationSet = (): IInstalledIntegrationSet => { const packageMap: PackageMap = new Map([]); + const addPackage = (fleetPackage: PackageListItem): void => { + if (fleetPackage.type !== 'integration') { + return; + } + if (fleetPackage.status !== 'installed') { + return; + } + + const packageKey = `${fleetPackage.name}`; + const existingPackageInfo = packageMap.get(packageKey); + + if (existingPackageInfo != null) { + return; + } + + // Actual `installed_version` is buried in SO, root `version` is latest package version available + const installedPackageVersion = fleetPackage.savedObject.attributes.install_version; + + // Policy templates correspond to package's integrations. + const packagePolicyTemplates = fleetPackage.policy_templates ?? []; + + const packageInfo: PackageInfo = { + package_name: fleetPackage.name, + package_title: fleetPackage.title, + package_version: installedPackageVersion, + + integrations: new Map( + packagePolicyTemplates.map((pt) => { + const integrationTitle: string = + packagePolicyTemplates.length === 1 && pt.name === fleetPackage.name + ? fleetPackage.title + : pt.title; + + const integrationInfo: InstalledIntegrationBasicInfo = { + integration_name: pt.name, + integration_title: integrationTitle, + is_enabled: false, // There might not be an integration policy, so default false and later update in addPackagePolicy() + }; + + return [integrationInfo.integration_name, integrationInfo]; + }) + ), + }; + + packageMap.set(packageKey, packageInfo); + }; + const addPackagePolicy = (policy: PackagePolicy): void => { const packageInfo = getPackageInfoFromPolicy(policy); const integrationsInfo = getIntegrationsInfoFromPolicy(policy, packageInfo); - const packageKey = `${packageInfo.package_name}:${packageInfo.package_version}`; + const packageKey = `${packageInfo.package_name}`; const existingPackageInfo = packageMap.get(packageKey); if (existingPackageInfo == null) { @@ -56,21 +103,6 @@ export const createInstalledIntegrationSet = (): IInstalledIntegrationSet => { } }; - const addRegistryPackage = (registryPackage: ArchivePackage): 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 => { @@ -106,8 +138,8 @@ export const createInstalledIntegrationSet = (): IInstalledIntegrationSet => { }; return { + addPackage, addPackagePolicy, - addRegistryPackage, getPackages, getIntegrations, }; @@ -125,15 +157,30 @@ const getIntegrationsInfoFromPolicy = ( policy: PackagePolicy, packageInfo: InstalledPackageBasicInfo ): InstalledIntegrationBasicInfo[] => { - return policy.inputs.map((input) => { + // Construct integration info from the available policy_templates + const integrationInfos = policy.inputs.map((input) => { const integrationName = normalizeString(input.policy_template ?? input.type); // e.g. 'cloudtrail' const integrationTitle = `${packageInfo.package_title} ${capitalize(integrationName)}`; // e.g. 'AWS Cloudtrail' return { integration_name: integrationName, - integration_title: integrationTitle, // title gets re-initialized later in addRegistryPackage() + integration_title: integrationTitle, is_enabled: input.enabled, }; }); + + // Base package may not have policy template, so pull directly from `policy.package` if so + return [ + ...integrationInfos, + ...(policy.package + ? [ + { + integration_name: policy.package.name, + integration_title: policy.package.title, + is_enabled: true, // Always true if `policy.package` exists since this corresponds to the base package + }, + ] + : []), + ]; }; const normalizeString = (raw: string | null | undefined): string => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts index 7b904c282e1e4..559abe391f5a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts @@ -7,7 +7,6 @@ import type { Logger } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { initPromisePool } from '../../../../../utils/promise_pool'; import { buildSiemResponse } from '../../../routes/utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; @@ -15,8 +14,6 @@ import type { GetInstalledIntegrationsResponse } from '../../../../../../common/ import { GET_INSTALLED_INTEGRATIONS_URL } from '../../../../../../common/detection_engine/fleet_integrations'; import { createInstalledIntegrationSet } from './installed_integration_set'; -const MAX_CONCURRENT_REQUESTS_TO_PACKAGE_REGISTRY = 5; - /** * Returns an array of installed Fleet integrations and their packages. */ @@ -40,48 +37,18 @@ export const getInstalledIntegrationsRoute = ( const fleet = ctx.securitySolution.getInternalFleetServices(); const set = createInstalledIntegrationSet(); - const packagePolicies = await fleet.packagePolicy.list(fleet.internalReadonlySoClient, {}); + // Pulls all packages into memory just like the main fleet landing page + // No pagination support currently, so cannot batch this call + const allThePackages = await fleet.packages.getPackages(); + allThePackages.forEach((fleetPackage) => { + set.addPackage(fleetPackage); + }); + const packagePolicies = await fleet.packagePolicy.list(fleet.internalReadonlySoClient, {}); packagePolicies.items.forEach((policy) => { set.addPackagePolicy(policy); }); - const registryPackages = await initPromisePool({ - concurrency: MAX_CONCURRENT_REQUESTS_TO_PACKAGE_REGISTRY, - items: set.getPackages(), - executor: async (packageInfo) => { - const registryPackage = await fleet.packages.getPackage( - packageInfo.package_name, - packageInfo.package_version - ); - return registryPackage; - }, - }); - - if (registryPackages.errors.length > 0) { - const errors = registryPackages.errors.map(({ error, item }) => { - return { - error, - packageId: `${item.package_name}@${item.package_version}`, - }; - }); - - const packages = errors.map((e) => e.packageId).join(', '); - logger.error( - `Unable to retrieve installed integrations. Error fetching packages from registry: ${packages}.` - ); - - errors.forEach(({ error, packageId }) => { - const logMessage = `Error fetching package info from registry for ${packageId}`; - const logReason = error instanceof Error ? error.message : String(error); - logger.debug(`${logMessage}. ${logReason}`); - }); - } - - registryPackages.results.forEach(({ result }) => { - set.addRegistryPackage(result.packageInfo); - }); - const installedIntegrations = set.getIntegrations(); const body: GetInstalledIntegrationsResponse = {