[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:
Kibana Machine 2023-02-28 22:16:50 -05:00 committed by GitHub
parent 546f90360e
commit dd031558b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 63 deletions

View file

@ -12,6 +12,7 @@ const createClientMock = (): jest.Mocked<PackageClient> => ({
ensureInstalledPackage: jest.fn(),
fetchFindLatestPackage: jest.fn(),
getPackage: jest.fn(),
getPackages: jest.fn(),
reinstallEsAssets: jest.fn(),
});

View file

@ -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[]

View file

@ -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 },

View file

@ -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 => {

View file

@ -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 = {