mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.6] [Security Solution] Fixes Related Integrations showing as not installed or enabled when they actually are (#152055) (#152411)
# Backport This will backport the following commits from `main` to `8.6`: - [[Security Solution] Fixes Related Integrations showing as not installed or enabled when they actually are (#152055)](https://github.com/elastic/kibana/pull/152055) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Garrett Spong","email":"spong@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-03-01T01:47:28Z","message":"[Security Solution] Fixes Related Integrations showing as not installed or enabled when they actually are (#152055)\n\n## Summary\r\n\r\nResolves: https://github.com/elastic/kibana/issues/142081\r\nhttps://github.com/elastic/kibana/issues/149970\r\nhttps://github.com/elastic/kibana/issues/150968\r\n\r\nBy adding an initial query for installed integrations and augments the\r\nexisting `InstalledIntegrationArray` constructed using\r\n`PackagePolicy`'s. Also removes `version` from the `packageKey` when\r\ncalculating installed integrations as there can be mis-matches between\r\ndifferent policy versions and the integration itself, and I believe the\r\nintended behavior here is to not have multiple `relatedIntegrations`\r\nreturned for different versions. We may want to expand the response here\r\nto include all the different policy versions that exist (and perhaps #\r\nof agents assigned the policy).\r\n\r\nLastly, updates `getIntegrationsInfoFromPolicy()` to also pull the base\r\n`package` details in addition to the policy_template details, as this is\r\nwhat ensure base packages show as `Installed: enabled` if they have an\r\nintegration policy assigned (vs just showing as `Installed` like when\r\nthere isn't an integration policy).\r\n\r\nNote: This PR also adds the `getPackages()` method to the\r\n`PackageClient` as it didn't currently exist, and was only available via\r\nthe fleet API via the `/api/fleet/epm/packages` route.\r\n\r\n\r\n### Before:\r\n<p align=\"center\">\r\n<img width=\"500\"\r\nsrc=\"https://user-images.githubusercontent.com/2946766/221066781-be7aa1c6-1728-4200-98b2-d40946e48bbe.png\"\r\n/>\r\n</p>\r\n\r\n\r\n\r\n### After\r\n<p align=\"center\">\r\n<img width=\"500\"\r\nsrc=\"https://user-images.githubusercontent.com/2946766/221323469-e24081f9-0741-41fd-8227-9e319c98b0d3.png\"\r\n/>\r\n</p>\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>","sha":"b833b108215c858177535fd3feee406cc628bca8","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Team:Fleet","Team:Detections and Resp","Team: SecuritySolution","Team:Detection Rules","v8.7.0","v8.8.0","v8.6.3","Feature:Related Integrations"],"number":152055,"url":"https://github.com/elastic/kibana/pull/152055","mergeCommit":{"message":"[Security Solution] Fixes Related Integrations showing as not installed or enabled when they actually are (#152055)\n\n## Summary\r\n\r\nResolves: https://github.com/elastic/kibana/issues/142081\r\nhttps://github.com/elastic/kibana/issues/149970\r\nhttps://github.com/elastic/kibana/issues/150968\r\n\r\nBy adding an initial query for installed integrations and augments the\r\nexisting `InstalledIntegrationArray` constructed using\r\n`PackagePolicy`'s. Also removes `version` from the `packageKey` when\r\ncalculating installed integrations as there can be mis-matches between\r\ndifferent policy versions and the integration itself, and I believe the\r\nintended behavior here is to not have multiple `relatedIntegrations`\r\nreturned for different versions. We may want to expand the response here\r\nto include all the different policy versions that exist (and perhaps #\r\nof agents assigned the policy).\r\n\r\nLastly, updates `getIntegrationsInfoFromPolicy()` to also pull the base\r\n`package` details in addition to the policy_template details, as this is\r\nwhat ensure base packages show as `Installed: enabled` if they have an\r\nintegration policy assigned (vs just showing as `Installed` like when\r\nthere isn't an integration policy).\r\n\r\nNote: This PR also adds the `getPackages()` method to the\r\n`PackageClient` as it didn't currently exist, and was only available via\r\nthe fleet API via the `/api/fleet/epm/packages` route.\r\n\r\n\r\n### Before:\r\n<p align=\"center\">\r\n<img width=\"500\"\r\nsrc=\"https://user-images.githubusercontent.com/2946766/221066781-be7aa1c6-1728-4200-98b2-d40946e48bbe.png\"\r\n/>\r\n</p>\r\n\r\n\r\n\r\n### After\r\n<p align=\"center\">\r\n<img width=\"500\"\r\nsrc=\"https://user-images.githubusercontent.com/2946766/221323469-e24081f9-0741-41fd-8227-9e319c98b0d3.png\"\r\n/>\r\n</p>\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>","sha":"b833b108215c858177535fd3feee406cc628bca8"}},"sourceBranch":"main","suggestedTargetBranches":["8.7","8.6"],"targetPullRequestStates":[{"branch":"8.7","label":"v8.7.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/152055","number":152055,"mergeCommit":{"message":"[Security Solution] Fixes Related Integrations showing as not installed or enabled when they actually are (#152055)\n\n## Summary\r\n\r\nResolves: https://github.com/elastic/kibana/issues/142081\r\nhttps://github.com/elastic/kibana/issues/149970\r\nhttps://github.com/elastic/kibana/issues/150968\r\n\r\nBy adding an initial query for installed integrations and augments the\r\nexisting `InstalledIntegrationArray` constructed using\r\n`PackagePolicy`'s. Also removes `version` from the `packageKey` when\r\ncalculating installed integrations as there can be mis-matches between\r\ndifferent policy versions and the integration itself, and I believe the\r\nintended behavior here is to not have multiple `relatedIntegrations`\r\nreturned for different versions. We may want to expand the response here\r\nto include all the different policy versions that exist (and perhaps #\r\nof agents assigned the policy).\r\n\r\nLastly, updates `getIntegrationsInfoFromPolicy()` to also pull the base\r\n`package` details in addition to the policy_template details, as this is\r\nwhat ensure base packages show as `Installed: enabled` if they have an\r\nintegration policy assigned (vs just showing as `Installed` like when\r\nthere isn't an integration policy).\r\n\r\nNote: This PR also adds the `getPackages()` method to the\r\n`PackageClient` as it didn't currently exist, and was only available via\r\nthe fleet API via the `/api/fleet/epm/packages` route.\r\n\r\n\r\n### Before:\r\n<p align=\"center\">\r\n<img width=\"500\"\r\nsrc=\"https://user-images.githubusercontent.com/2946766/221066781-be7aa1c6-1728-4200-98b2-d40946e48bbe.png\"\r\n/>\r\n</p>\r\n\r\n\r\n\r\n### After\r\n<p align=\"center\">\r\n<img width=\"500\"\r\nsrc=\"https://user-images.githubusercontent.com/2946766/221323469-e24081f9-0741-41fd-8227-9e319c98b0d3.png\"\r\n/>\r\n</p>\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>","sha":"b833b108215c858177535fd3feee406cc628bca8"}},{"branch":"8.6","label":"v8.6.3","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
parent
546f90360e
commit
dd031558b2
5 changed files with 102 additions and 63 deletions
|
@ -12,6 +12,7 @@ const createClientMock = (): jest.Mocked<PackageClient> => ({
|
|||
ensureInstalledPackage: jest.fn(),
|
||||
fetchFindLatestPackage: jest.fn(),
|
||||
getPackage: jest.fn(),
|
||||
getPackages: jest.fn(),
|
||||
reinstallEsAssets: jest.fn(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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<PackageList>;
|
||||
|
||||
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[]
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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<string, PackageInfo>([]);
|
||||
|
||||
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<string, InstalledIntegrationBasicInfo>(
|
||||
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 => {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue