[Security Solution][Detections] Implement endpoint for fetching installed Fleet integrations (#132667)

**Addresses partially:** https://github.com/elastic/security-team/issues/2856, https://github.com/elastic/security-team/issues/3624 (internal tickets)

## Summary

Adds a new detections endpoint that returns installed Fleet integrations. It is to be used on the Rule Management and Rule Details pages (see https://github.com/elastic/kibana/pull/131475 for context and screenshots). This endpoint is `internal` - no need to document it.

```
GET /internal/detection_engine/fleet/integrations/installed
```

```json
{
  "installed_integrations": [
    {
      "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": "dynamodb",
      "integration_title": "Amazon DynamoDB",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "ebs",
      "integration_title": "Amazon EBS",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "ec2",
      "integration_title": "Amazon EC2",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "elb",
      "integration_title": "AWS ELB",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "lambda",
      "integration_title": "AWS Lambda",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "natgateway",
      "integration_title": "Amazon NAT Gateway",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "firewall",
      "integration_title": "AWS Network Firewall",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "rds",
      "integration_title": "Amazon RDS",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "s3",
      "integration_title": "Amazon S3",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "s3_storage_lens",
      "integration_title": "Amazon S3 Storage Lens",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "sns",
      "integration_title": "Amazon SNS",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "sqs",
      "integration_title": "Amazon SQS",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "transitgateway",
      "integration_title": "AWS Transit Gateway",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "usage",
      "integration_title": "AWS Usage",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "vpcflow",
      "integration_title": "Amazon VPC",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "vpn",
      "integration_title": "Amazon VPN",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "waf",
      "integration_title": "AWS WAF",
      "is_enabled": false
    },
    {
      "package_name": "aws",
      "package_title": "AWS",
      "package_version": "1.16.1",
      "integration_name": "route53",
      "integration_title": "AWS Route 53",
      "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
    }
  ]
}
```

## Next steps

- Test with users with different privileges (non-superusers). Fleet privileges: none, read, all. Security Solution privileges. SO privileges.
- Add filtering by `package_name` and `is_enabled` (will be done in a separate PR).
- Add test coverage (will be done in a separate PR).


### Checklist

Delete any items that are not applicable to this PR.

- [ ] [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
This commit is contained in:
Georgii Gorbachev 2022-05-24 01:36:54 +02:00 committed by GitHub
parent 93de448327
commit dcf83f9f85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 508 additions and 2 deletions

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export * from './installed_integrations';
export * from './rule_monitoring';
export * from './rule_params';
export * from './schemas';

View file

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

View file

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

View file

@ -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');
}),
};
};

View file

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

View file

@ -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<string, PackageInfo>;
interface PackageInfo extends InstalledPackageBasicInfo {
integrations: Map<string, InstalledIntegrationBasicInfo>;
}
export const createInstalledIntegrationSet = (): IInstalledIntegrationSet => {
const packageMap: PackageMap = new Map<string, PackageInfo>([]);
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<string, InstalledIntegrationBasicInfo>();
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<string, InstalledIntegrationBasicInfo>,
integration: InstalledIntegrationBasicInfo
): void => {
if (!map.has(integration.integration_name) || integration.is_enabled) {
map.set(integration.integration_name, integration);
}
};

View file

@ -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<SecuritySolutionRequestHandlerContext>();
core.http.registerRouteHandlerContext<SecuritySolutionRequestHandlerContext, typeof APP_ID>(
APP_ID,

View file

@ -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<SecuritySolutionApiRequestHandlerContext> {
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)
),
};
}
}

View file

@ -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);

View file

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