[Security Solution] Initial rule upgrade/install endpoints implementation (#155517)

**Resolves: https://github.com/elastic/kibana/issues/148186**
**Resolves (partially):
https://github.com/elastic/kibana/issues/148184**
This commit is contained in:
Dmitrii Shevchenko 2023-05-26 14:21:59 +02:00 committed by GitHub
parent 33f5bb6ba5
commit 74d276ed0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 967 additions and 420 deletions

View file

@ -6,12 +6,8 @@
*/
export interface GetPrebuiltRulesStatusResponseBody {
status_code: number;
message: string;
attributes: {
/** Aggregated info about all prebuilt rules */
stats: PrebuiltRulesStatusStats;
};
/** Aggregated info about all prebuilt rules */
stats: PrebuiltRulesStatusStats;
}
export interface PrebuiltRulesStatusStats {

View file

@ -0,0 +1,37 @@
/*
* 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 * as t from 'io-ts';
export const RuleVersionSpecifier = t.exact(
t.type({
rule_id: t.string,
version: t.number,
})
);
export type RuleVersionSpecifier = t.TypeOf<typeof RuleVersionSpecifier>;
export const InstallSpecificRulesRequest = t.exact(
t.type({
mode: t.literal(`SPECIFIC_RULES`),
rules: t.array(RuleVersionSpecifier),
})
);
export const InstallAllRulesRequest = t.exact(
t.type({
mode: t.literal(`ALL_RULES`),
})
);
export const PerformRuleInstallationRequestBody = t.union([
InstallAllRulesRequest,
InstallSpecificRulesRequest,
]);
export type PerformRuleInstallationRequestBody = t.TypeOf<
typeof PerformRuleInstallationRequestBody
>;

View file

@ -0,0 +1,32 @@
/*
* 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 type { RuleResponse } from '../../../rule_schema/model/rule_schemas';
import type { AggregatedPrebuiltRuleError } from '../../model/prebuilt_rules/aggregated_prebuilt_rules_error';
export enum SkipRuleInstallReason {
ALREADY_INSTALLED = 'ALREADY_INSTALLED',
}
export interface SkippedRuleInstall {
rule_id: string;
reason: SkipRuleInstallReason;
}
export interface PerformRuleInstallationResponseBody {
summary: {
total: number;
succeeded: number;
skipped: number;
failed: number;
};
results: {
created: RuleResponse[];
skipped: SkippedRuleInstall[];
};
errors: AggregatedPrebuiltRuleError[];
}

View file

@ -0,0 +1,71 @@
/*
* 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 { enumeration } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';
export enum PickVersionValues {
BASE = 'BASE',
CURRENT = 'CURRENT',
TARGET = 'TARGET',
}
export const TPickVersionValues = enumeration('PickVersionValues', PickVersionValues);
export const RuleUpgradeSpecifier = t.exact(
t.intersection([
t.type({
rule_id: t.string,
/**
* This parameter is needed for handling race conditions with Optimistic Concurrency Control.
* Two or more users can call installation/_review and installation/_perform endpoints concurrently.
* Also, in general the time between these two calls can be anything.
* The idea is to only allow the user to install a rule if the user has reviewed the exact version
* of it that had been returned from the _review endpoint. If the version changed on the BE,
* installation/_perform endpoint will return a version mismatch error for this rule.
*/
revision: t.number,
/**
* The target version to upgrade to.
*/
version: t.number,
}),
t.partial({
pick_version: TPickVersionValues,
}),
])
);
export type RuleUpgradeSpecifier = t.TypeOf<typeof RuleUpgradeSpecifier>;
export const UpgradeSpecificRulesRequest = t.exact(
t.intersection([
t.type({
mode: t.literal(`SPECIFIC_RULES`),
rules: t.array(RuleUpgradeSpecifier),
}),
t.partial({
pick_version: TPickVersionValues,
}),
])
);
export const UpgradeAllRulesRequest = t.exact(
t.intersection([
t.type({
mode: t.literal(`ALL_RULES`),
}),
t.partial({
pick_version: TPickVersionValues,
}),
])
);
export const PerformRuleUpgradeRequestBody = t.union([
UpgradeAllRulesRequest,
UpgradeSpecificRulesRequest,
]);
export type PerformRuleUpgradeRequestBody = t.TypeOf<typeof PerformRuleUpgradeRequestBody>;

View file

@ -0,0 +1,32 @@
/*
* 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 type { RuleResponse } from '../../../rule_schema';
import type { AggregatedPrebuiltRuleError } from '../../model/prebuilt_rules/aggregated_prebuilt_rules_error';
export enum SkipRuleUpgradeReason {
RULE_UP_TO_DATE = 'RULE_UP_TO_DATE',
}
export interface SkippedRuleUpgrade {
rule_id: string;
reason: SkipRuleUpgradeReason;
}
export interface PerformRuleUpgradeResponseBody {
summary: {
total: number;
succeeded: number;
skipped: number;
failed: number;
};
results: {
updated: RuleResponse[];
skipped: SkippedRuleUpgrade[];
};
errors: AggregatedPrebuiltRuleError[];
}

View file

@ -9,15 +9,11 @@ import type { RuleSignatureId, RuleTagArray, RuleVersion } from '../../../rule_s
import type { DiffableRule } from '../../model/diff/diffable_rule/diffable_rule';
export interface ReviewRuleInstallationResponseBody {
status_code: number;
message: string;
attributes: {
/** Aggregated info about all rules available for installation */
stats: RuleInstallationStatsForReview;
/** Aggregated info about all rules available for installation */
stats: RuleInstallationStatsForReview;
/** Info about individual rules: one object per each rule available for installation */
rules: RuleInstallationInfoForReview[];
};
/** Info about individual rules: one object per each rule available for installation */
rules: RuleInstallationInfoForReview[];
}
export interface RuleInstallationStatsForReview {

View file

@ -10,15 +10,11 @@ import type { DiffableRule } from '../../model/diff/diffable_rule/diffable_rule'
import type { PartialRuleDiff } from '../../model/diff/rule_diff/rule_diff';
export interface ReviewRuleUpgradeResponseBody {
status_code: number;
message: string;
attributes: {
/** Aggregated info about all rules available for upgrade */
stats: RuleUpgradeStatsForReview;
/** Aggregated info about all rules available for upgrade */
stats: RuleUpgradeStatsForReview;
/** Info about individual rules: one object per each rule available for upgrade */
rules: RuleUpgradeInfoForReview[];
};
/** Info about individual rules: one object per each rule available for upgrade */
rules: RuleUpgradeInfoForReview[];
}
export interface RuleUpgradeStatsForReview {

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
export interface AggregatedPrebuiltRuleError {
message: string;
status_code?: number;
rules: Array<{
rule_id: string;
name?: string;
}>;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { getPrebuiltRulesAndTimelinesStatusRoute } from './route';
import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status_route';
import {
getEmptyFindResult,

View file

@ -6,19 +6,13 @@
*/
import { transformError } from '@kbn/securitysolution-es-utils';
import { GET_PREBUILT_RULES_STATUS_URL } from '../../../../../../common/detection_engine/prebuilt_rules';
import type {
GetPrebuiltRulesStatusResponseBody,
PrebuiltRulesStatusStats,
} from '../../../../../../common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema';
import type { GetPrebuiltRulesStatusResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildSiemResponse } from '../../../routes/utils';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import type { VersionBuckets } from '../../model/rule_versions/get_version_buckets';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => {
@ -40,23 +34,18 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const [latestVersions, { installedVersions }] = await Promise.all([
ruleAssetsClient.fetchLatestVersions(),
ruleObjectsClient.fetchInstalledRules(),
]);
const versionBuckets = getVersionBuckets({
latestVersions,
installedVersions,
const ruleVersionsMap = await fetchRuleVersionsTriad({
ruleAssetsClient,
ruleObjectsClient,
});
const stats = calculateRuleStats(versionBuckets);
const { currentRules, installableRules, upgradeableRules } =
getVersionBuckets(ruleVersionsMap);
const body: GetPrebuiltRulesStatusResponseBody = {
status_code: 200,
message: 'OK',
attributes: {
stats,
stats: {
num_prebuilt_rules_installed: currentRules.length,
num_prebuilt_rules_to_install: installableRules.length,
num_prebuilt_rules_to_upgrade: upgradeableRules.length,
},
};
@ -71,13 +60,3 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
}
);
};
const calculateRuleStats = (buckets: VersionBuckets): PrebuiltRulesStatusStats => {
const { installedVersions, latestVersionsToInstall, installedVersionsToUpgrade } = buckets;
return {
num_prebuilt_rules_installed: installedVersions.length,
num_prebuilt_rules_to_install: latestVersionsToInstall.length,
num_prebuilt_rules_to_upgrade: installedVersionsToUpgrade.length,
};
};

View file

@ -13,7 +13,10 @@ import {
getBasicEmptySearchResponse,
} from '../../../routes/__mocks__/request_responses';
import { requestContextMock, serverMock } from '../../../routes/__mocks__';
import { installPrebuiltRulesAndTimelinesRoute, createPrepackagedRules } from './route';
import {
installPrebuiltRulesAndTimelinesRoute,
createPrepackagedRules,
} from './install_prebuilt_rules_and_timelines_route';
import { listMock } from '@kbn/lists-plugin/server/mocks';
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
import { installPrepackagedTimelines } from '../../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines';

View file

@ -5,33 +5,30 @@
* 2.0.
*/
import moment from 'moment';
import { transformError } from '@kbn/securitysolution-es-utils';
import { validate } from '@kbn/securitysolution-io-ts-utils';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { validate } from '@kbn/securitysolution-io-ts-utils';
import moment from 'moment';
import {
InstallPrebuiltRulesAndTimelinesResponse,
PREBUILT_RULES_URL,
} from '../../../../../../common/detection_engine/prebuilt_rules';
import { importTimelineResultSchema } from '../../../../../../common/types/timeline';
import type {
SecuritySolutionApiRequestHandlerContext,
SecuritySolutionPluginRouter,
} from '../../../../../types';
import {
PREBUILT_RULES_URL,
InstallPrebuiltRulesAndTimelinesResponse,
} from '../../../../../../common/detection_engine/prebuilt_rules';
import { importTimelineResultSchema } from '../../../../../../common/types/timeline';
import { installPrepackagedTimelines } from '../../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines';
import { buildSiemResponse } from '../../../routes/utils';
import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules';
import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_rules';
import { updatePrebuiltRules } from '../../logic/rule_objects/update_prebuilt_rules';
import { getRulesToInstall } from '../../logic/get_rules_to_install';
import { getRulesToUpdate } from '../../logic/get_rules_to_update';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_rules';
import { upgradePrebuiltRules } from '../../logic/rule_objects/upgrade_prebuilt_rules';
import { rulesToMap } from '../../logic/utils';
import { installPrepackagedTimelines } from '../../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines';
import { buildSiemResponse } from '../../../routes/utils';
import { installPrebuiltRulesPackage } from './install_prebuilt_rules_package';
import { ensureLatestRulesPackageInstalled } from '../../logic/ensure_latest_rules_package_installed';
export const installPrebuiltRulesAndTimelinesRoute = (router: SecuritySolutionPluginRouter) => {
router.put(
@ -103,20 +100,20 @@ export const createPrepackagedRules = async (
await exceptionsListClient.createEndpointList();
}
let latestPrebuiltRules = await ruleAssetsClient.fetchLatestAssets();
if (latestPrebuiltRules.length === 0) {
// Seems no packages with prepackaged rules were installed, try to install the default rules package
await installPrebuiltRulesPackage(config, context);
// Try to get the prepackaged rules again
latestPrebuiltRules = await ruleAssetsClient.fetchLatestAssets();
}
const latestPrebuiltRules = await ensureLatestRulesPackageInstalled(
ruleAssetsClient,
config,
context
);
const installedPrebuiltRules = rulesToMap(await getExistingPrepackagedRules({ rulesClient }));
const rulesToInstall = getRulesToInstall(latestPrebuiltRules, installedPrebuiltRules);
const rulesToUpdate = getRulesToUpdate(latestPrebuiltRules, installedPrebuiltRules);
await createPrebuiltRules(rulesClient, rulesToInstall);
const result = await createPrebuiltRules(rulesClient, rulesToInstall);
if (result.errors.length > 0) {
throw new AggregateError(result.errors, 'Error installing new prebuilt rules');
}
const timeline = await installPrepackagedTimelines(
maxTimelineImportExportSize,
@ -128,7 +125,7 @@ export const createPrepackagedRules = async (
importTimelineResultSchema
);
await updatePrebuiltRules(rulesClient, savedObjectsClient, rulesToUpdate);
await upgradePrebuiltRules(rulesClient, rulesToUpdate);
const prebuiltRulesOutput: InstallPrebuiltRulesAndTimelinesResponse = {
rules_installed: rulesToInstall.length,

View file

@ -0,0 +1,127 @@
/*
* 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 { PERFORM_RULE_INSTALLATION_URL } from '../../../../../../common/detection_engine/prebuilt_rules';
import { PerformRuleInstallationRequestBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema';
import type {
PerformRuleInstallationResponseBody,
SkippedRuleInstall,
} from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema';
import { SkipRuleInstallReason } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildRouteValidation } from '../../../../../utils/build_validation/route_validation';
import type { PromisePoolError } from '../../../../../utils/promise_pool';
import { buildSiemResponse } from '../../../routes/utils';
import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters';
import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors';
import { ensureLatestRulesPackageInstalled } from '../../logic/ensure_latest_rules_package_installed';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_rules';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
export const performRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => {
router.post(
{
path: PERFORM_RULE_INSTALLATION_URL,
validate: {
body: buildRouteValidation(PerformRuleInstallationRequestBody),
},
options: {
tags: ['access:securitySolution'],
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
const config = ctx.securitySolution.getConfig();
const soClient = ctx.core.savedObjects.client;
const rulesClient = ctx.alerting.getRulesClient();
const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const exceptionsListClient = ctx.securitySolution.getExceptionListClient();
const { mode } = request.body;
// This will create the endpoint list if it does not exist yet
await exceptionsListClient?.createEndpointList();
// If this API is used directly without hitting any detection engine
// pages first, the rules package might be missing.
await ensureLatestRulesPackageInstalled(ruleAssetsClient, config, ctx.securitySolution);
const fetchErrors: Array<PromisePoolError<{ rule_id: string }>> = [];
const skippedRules: SkippedRuleInstall[] = [];
const ruleVersionsMap = await fetchRuleVersionsTriad({
ruleAssetsClient,
ruleObjectsClient,
versionSpecifiers: mode === 'ALL_RULES' ? undefined : request.body.rules,
});
const { currentRules, installableRules } = getVersionBuckets(ruleVersionsMap);
// Perform all the checks we can before we start the upgrade process
if (mode === 'SPECIFIC_RULES') {
const currentRuleIds = new Set(currentRules.map((rule) => rule.rule_id));
const installableRuleIds = new Set(installableRules.map((rule) => rule.rule_id));
request.body.rules.forEach((rule) => {
// Check that the requested rule is not installed yet
if (currentRuleIds.has(rule.rule_id)) {
skippedRules.push({
rule_id: rule.rule_id,
reason: SkipRuleInstallReason.ALREADY_INSTALLED,
});
return;
}
// Check that the requested rule is installable
if (!installableRuleIds.has(rule.rule_id)) {
fetchErrors.push({
error: new Error(
`Rule with ID "${rule.rule_id}" and version "${rule.version}" not found`
),
item: rule,
});
}
});
}
const { results: installedRules, errors: installationErrors } = await createPrebuiltRules(
rulesClient,
installableRules
);
const combinedErrors = [...fetchErrors, ...installationErrors];
const body: PerformRuleInstallationResponseBody = {
summary: {
total: installedRules.length + skippedRules.length + combinedErrors.length,
succeeded: installedRules.length,
skipped: skippedRules.length,
failed: combinedErrors.length,
},
results: {
created: installedRules.map(({ result }) => internalRuleToAPIResponse(result)),
skipped: skippedRules,
},
errors: aggregatePrebuiltRuleErrors(combinedErrors),
};
return response.ok({ body });
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -0,0 +1,176 @@
/*
* 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 { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/detection_engine/prebuilt_rules';
import {
PerformRuleUpgradeRequestBody,
PickVersionValues,
} from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema';
import type {
PerformRuleUpgradeResponseBody,
SkippedRuleUpgrade,
} from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema';
import { SkipRuleUpgradeReason } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema';
import { assertUnreachable } from '../../../../../../common/utility_types';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildRouteValidation } from '../../../../../utils/build_validation/route_validation';
import type { PromisePoolError } from '../../../../../utils/promise_pool';
import { buildSiemResponse } from '../../../routes/utils';
import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters';
import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { upgradePrebuiltRules } from '../../logic/rule_objects/upgrade_prebuilt_rules';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => {
router.post(
{
path: PERFORM_RULE_UPGRADE_URL,
validate: {
body: buildRouteValidation(PerformRuleUpgradeRequestBody),
},
options: {
tags: ['access:securitySolution'],
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
const ctx = await context.resolve(['core', 'alerting']);
const soClient = ctx.core.savedObjects.client;
const rulesClient = ctx.alerting.getRulesClient();
const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const { mode, pick_version: globalPickVersion = PickVersionValues.TARGET } = request.body;
const fetchErrors: Array<PromisePoolError<{ rule_id: string }>> = [];
const targetRules: PrebuiltRuleAsset[] = [];
const skippedRules: SkippedRuleUpgrade[] = [];
const versionSpecifiers = mode === 'ALL_RULES' ? undefined : request.body.rules;
const versionSpecifiersMap = new Map(
versionSpecifiers?.map((rule) => [rule.rule_id, rule])
);
const ruleVersionsMap = await fetchRuleVersionsTriad({
ruleAssetsClient,
ruleObjectsClient,
versionSpecifiers,
});
const versionBuckets = getVersionBuckets(ruleVersionsMap);
const { currentRules } = versionBuckets;
// The upgradeable rules list is mutable; we can remove rules from it because of version mismatch
let upgradeableRules = versionBuckets.upgradeableRules;
// Perform all the checks we can before we start the upgrade process
if (mode === 'SPECIFIC_RULES') {
const installedRuleIds = new Set(currentRules.map((rule) => rule.rule_id));
const upgradeableRuleIds = new Set(
upgradeableRules.map(({ current }) => current.rule_id)
);
request.body.rules.forEach((rule) => {
// Check that the requested rule was found
if (!installedRuleIds.has(rule.rule_id)) {
fetchErrors.push({
error: new Error(
`Rule with ID "${rule.rule_id}" and version "${rule.version}" not found`
),
item: rule,
});
return;
}
// Check that the requested rule is upgradeable
if (!upgradeableRuleIds.has(rule.rule_id)) {
skippedRules.push({
rule_id: rule.rule_id,
reason: SkipRuleUpgradeReason.RULE_UP_TO_DATE,
});
return;
}
// Check that rule revisions match (no update slipped in since the user reviewed the list)
const currentRevision = ruleVersionsMap.get(rule.rule_id)?.current?.revision;
if (rule.revision !== currentRevision) {
fetchErrors.push({
error: new Error(
`Revision mismatch for rule ID ${rule.rule_id}: expected ${rule.revision}, got ${currentRevision}`
),
item: rule,
});
// Remove the rule from the list of upgradeable rules
upgradeableRules = upgradeableRules.filter(
({ current }) => current.rule_id !== rule.rule_id
);
}
});
}
// Construct the list of target rule versions
upgradeableRules.forEach(({ current, target }) => {
const rulePickVersion =
versionSpecifiersMap?.get(current.rule_id)?.pick_version ?? globalPickVersion;
switch (rulePickVersion) {
case PickVersionValues.BASE:
const baseVersion = ruleVersionsMap.get(current.rule_id)?.base;
if (baseVersion) {
targetRules.push({ ...baseVersion, version: target.version });
} else {
fetchErrors.push({
error: new Error(`Could not find base version for rule ${current.rule_id}`),
item: current,
});
}
break;
case PickVersionValues.CURRENT:
targetRules.push({ ...current, version: target.version });
break;
case PickVersionValues.TARGET:
targetRules.push(target);
break;
default:
assertUnreachable(rulePickVersion);
}
});
// Perform the upgrade
const { results: updatedRules, errors: installationErrors } = await upgradePrebuiltRules(
rulesClient,
targetRules
);
const combinedErrors = [...fetchErrors, ...installationErrors];
const body: PerformRuleUpgradeResponseBody = {
summary: {
total: updatedRules.length + skippedRules.length + combinedErrors.length,
skipped: skippedRules.length,
succeeded: updatedRules.length,
failed: combinedErrors.length,
},
results: {
updated: updatedRules.map(({ result }) => internalRuleToAPIResponse(result)),
skipped: skippedRules,
},
errors: aggregatePrebuiltRuleErrors(combinedErrors),
};
return response.ok({ body });
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -9,12 +9,14 @@ import type { ConfigType } from '../../../../config';
import type { SetupPlugins } from '../../../../plugin_contract';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status/route';
import { getPrebuiltRulesStatusRoute } from './get_prebuilt_rules_status/route';
import { installPrebuiltRulesAndTimelinesRoute } from './install_prebuilt_rules_and_timelines/route';
import { generateAssetsRoute } from './generate_assets/route';
import { reviewRuleInstallationRoute } from './review_rule_installation/route';
import { reviewRuleUpgradeRoute } from './review_rule_upgrade/route';
import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route';
import { getPrebuiltRulesStatusRoute } from './get_prebuilt_rules_status/get_prebuilt_rules_status_route';
import { installPrebuiltRulesAndTimelinesRoute } from './install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route';
import { generateAssetsRoute } from './generate_assets/generate_assets_route';
import { reviewRuleInstallationRoute } from './review_rule_installation/review_rule_installation_route';
import { reviewRuleUpgradeRoute } from './review_rule_upgrade/review_rule_upgrade_route';
import { performRuleInstallationRoute } from './perform_rule_installation/perform_rule_installation_route';
import { performRuleUpgradeRoute } from './perform_rule_upgrade/perform_rule_upgrade_route';
export const registerPrebuiltRulesRoutes = (
router: SecuritySolutionPluginRouter,
@ -30,6 +32,8 @@ export const registerPrebuiltRulesRoutes = (
if (prebuiltRulesNewUpgradeAndInstallationWorkflowsEnabled) {
// New endpoints for the rule upgrade and installation workflows
getPrebuiltRulesStatusRoute(router);
performRuleInstallationRoute(router);
performRuleUpgradeRoute(router);
reviewRuleInstallationRoute(router);
reviewRuleUpgradeRoute(router);

View file

@ -6,21 +6,19 @@
*/
import { transformError } from '@kbn/securitysolution-es-utils';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/detection_engine/prebuilt_rules';
import type {
ReviewRuleInstallationResponseBody,
RuleInstallationInfoForReview,
RuleInstallationStatsForReview,
} from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildSiemResponse } from '../../../routes/utils';
import { convertRuleToDiffable } from '../../logic/diff/normalization/convert_rule_to_diffable';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => {
@ -42,27 +40,15 @@ export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter
const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const [latestVersions, { installedVersions }] = await Promise.all([
ruleAssetsClient.fetchLatestVersions(),
ruleObjectsClient.fetchInstalledRules(),
]);
const versionBuckets = getVersionBuckets({
latestVersions,
installedVersions,
const ruleVersionsMap = await fetchRuleVersionsTriad({
ruleAssetsClient,
ruleObjectsClient,
});
const rulesToInstall = await ruleAssetsClient.fetchAssetsByVersionInfo(
versionBuckets.latestVersionsToInstall
);
const { installableRules } = getVersionBuckets(ruleVersionsMap);
const body: ReviewRuleInstallationResponseBody = {
status_code: 200,
message: 'OK',
attributes: {
stats: calculateRuleStats(rulesToInstall),
rules: calculateRuleInfos(rulesToInstall),
},
stats: calculateRuleStats(installableRules),
rules: calculateRuleInfos(installableRules),
};
return response.ok({ body });

View file

@ -5,32 +5,24 @@
* 2.0.
*/
import { pickBy } from 'lodash';
import { transformError } from '@kbn/securitysolution-es-utils';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import { pickBy } from 'lodash';
import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/detection_engine/prebuilt_rules';
import type {
ReviewRuleUpgradeResponseBody,
RuleUpgradeInfoForReview,
RuleUpgradeStatsForReview,
} from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema';
import type { PrebuiltRuleVersionInfo } from '../../model/rule_versions/prebuilt_rule_version_info';
import type {
CalculateRuleDiffArgs,
CalculateRuleDiffResult,
} from '../../logic/diff/calculate_rule_diff';
import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff';
import type { ThreeWayDiff } from '../../../../../../common/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff';
import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema';
import { invariant } from '../../../../../../common/utils/invariant';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildSiemResponse } from '../../../routes/utils';
import type { CalculateRuleDiffResult } from '../../logic/diff/calculate_rule_diff';
import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
import { invariant } from '../../../../../../common/utils/invariant';
export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => {
router.post(
@ -51,38 +43,21 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) =>
const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const [latestVersions, { installedVersions, installedRules }] = await Promise.all([
ruleAssetsClient.fetchLatestVersions(),
ruleObjectsClient.fetchInstalledRules(),
]);
const versionBuckets = getVersionBuckets({
latestVersions,
installedVersions,
const ruleVersionsMap = await fetchRuleVersionsTriad({
ruleAssetsClient,
ruleObjectsClient,
});
const { upgradeableRules } = getVersionBuckets(ruleVersionsMap);
const [baseRules, latestRules] = await Promise.all([
ruleAssetsClient.fetchAssetsByVersionInfo(versionBuckets.installedVersionsToUpgrade),
ruleAssetsClient.fetchAssetsByVersionInfo(versionBuckets.latestVersionsToUpgrade),
]);
const ruleDiffCalculationArgs = getRuleDiffCalculationArgs(
versionBuckets.installedVersionsToUpgrade,
installedRules,
baseRules,
latestRules
);
const ruleDiffCalculationResults = ruleDiffCalculationArgs.map((args) => {
return calculateRuleDiff(args);
const ruleDiffCalculationResults = upgradeableRules.map(({ current }) => {
const ruleVersions = ruleVersionsMap.get(current.rule_id);
invariant(ruleVersions != null, 'ruleVersions not found');
return calculateRuleDiff(ruleVersions);
});
const body: ReviewRuleUpgradeResponseBody = {
status_code: 200,
message: 'OK',
attributes: {
stats: calculateRuleStats(ruleDiffCalculationResults),
rules: calculateRuleInfos(ruleDiffCalculationResults),
},
stats: calculateRuleStats(ruleDiffCalculationResults),
rules: calculateRuleInfos(ruleDiffCalculationResults),
};
return response.ok({ body });
@ -97,41 +72,9 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) =>
);
};
const getRuleDiffCalculationArgs = (
installedVersionsToUpgrade: PrebuiltRuleVersionInfo[],
installedRules: RuleResponse[],
baseRules: PrebuiltRuleAsset[],
latestRules: PrebuiltRuleAsset[]
): CalculateRuleDiffArgs[] => {
const installedRulesMap = new Map(installedRules.map((r) => [r.rule_id, r]));
const baseRulesMap = new Map(baseRules.map((r) => [r.rule_id, r]));
const latestRulesMap = new Map(latestRules.map((r) => [r.rule_id, r]));
const result: CalculateRuleDiffArgs[] = [];
installedVersionsToUpgrade.forEach((versionToUpgrade) => {
const ruleId = versionToUpgrade.rule_id;
const installedRule = installedRulesMap.get(ruleId);
const baseRule = baseRulesMap.get(ruleId);
const latestRule = latestRulesMap.get(ruleId);
// baseRule can be undefined if the rule has no historical versions, but other versions should always be present
invariant(installedRule != null, `installedRule is not found for rule_id: ${ruleId}`);
invariant(latestRule != null, `latestRule is not found for rule_id: ${ruleId}`);
result.push({
currentVersion: installedRule,
baseVersion: baseRule,
targetVersion: latestRule,
});
});
return result;
};
const calculateRuleStats = (results: CalculateRuleDiffResult[]): RuleUpgradeStatsForReview => {
const allTags = new Set<string>(
results.flatMap((result) => result.ruleVersions.input.current.tags)
results.flatMap((result) => result.ruleVersions.input.current?.tags ?? [])
);
return {
num_rules_to_upgrade_total: results.length,
@ -144,6 +87,7 @@ const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfo
const { ruleDiff, ruleVersions } = result;
const installedCurrentVersion = ruleVersions.input.current;
const diffableCurrentVersion = ruleVersions.output.current;
invariant(installedCurrentVersion != null, 'installedCurrentVersion not found');
return {
id: installedCurrentVersion.id,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export { createPrepackagedRules } from './api/install_prebuilt_rules_and_timelines/route';
export { createPrepackagedRules } from './api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route';
export { registerPrebuiltRulesRoutes } from './api/register_routes';
export { prebuiltRuleAssetType } from './logic/rule_assets/prebuilt_rule_assets_type';
export { PrebuiltRuleAsset } from './model/rule_assets/prebuilt_rule_asset';

View file

@ -0,0 +1,37 @@
/*
* 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 type { AggregatedPrebuiltRuleError } from '../../../../../common/detection_engine/prebuilt_rules/model/prebuilt_rules/aggregated_prebuilt_rules_error';
import { getErrorMessage, getErrorStatusCode } from '../../../../utils/error_helpers';
import type { PromisePoolError } from '../../../../utils/promise_pool';
export function aggregatePrebuiltRuleErrors(
errors: Array<PromisePoolError<{ rule_id: string; name?: string }>>
) {
const errorsByMessage: Record<string, AggregatedPrebuiltRuleError> = {};
errors.forEach(({ error, item }) => {
const message = getErrorMessage(error);
const statusCode = getErrorStatusCode(error);
const failedRule = {
rule_id: item.rule_id,
name: item.name,
};
if (errorsByMessage[message]) {
errorsByMessage[message].rules.push(failedRule);
} else {
errorsByMessage[message] = {
message,
status_code: statusCode,
rules: [failedRule],
};
}
});
return Object.values(errorsByMessage);
}

View file

@ -10,25 +10,22 @@ import type { FullRuleDiff } from '../../../../../../common/detection_engine/pre
import type { ThreeWayDiff } from '../../../../../../common/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff';
import { MissingVersion } from '../../../../../../common/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff';
import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema';
import { invariant } from '../../../../../../common/utils/invariant';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import { calculateRuleFieldsDiff } from './calculation/calculate_rule_fields_diff';
import { convertRuleToDiffable } from './normalization/convert_rule_to_diffable';
export interface CalculateRuleDiffArgs {
currentVersion: RuleResponse;
baseVersion?: PrebuiltRuleAsset;
targetVersion: PrebuiltRuleAsset;
export interface RuleVersions {
current?: RuleResponse;
base?: PrebuiltRuleAsset;
target?: PrebuiltRuleAsset;
}
export interface CalculateRuleDiffResult {
ruleDiff: FullRuleDiff;
ruleVersions: {
input: {
current: RuleResponse;
base?: PrebuiltRuleAsset;
target: PrebuiltRuleAsset;
};
input: RuleVersions;
output: {
current: DiffableRule;
base?: DiffableRule;
@ -39,11 +36,11 @@ export interface CalculateRuleDiffResult {
/**
* Calculates a rule diff for a given set of 3 versions of the rule:
* - currenly installed version
* - currently installed version
* - base version that is the corresponding stock rule content
* - target version which is the stock rule content the user wants to update the rule to
*/
export const calculateRuleDiff = (args: CalculateRuleDiffArgs): CalculateRuleDiffResult => {
export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult => {
/*
1. Convert current, base and target versions to `DiffableRule`.
2. Calculate a `RuleFieldsDiff`. For every top-level field of `DiffableRule`:
@ -59,11 +56,16 @@ export const calculateRuleDiff = (args: CalculateRuleDiffArgs): CalculateRuleDif
3. Create and return a result based on the `RuleFieldsDiff`.
*/
const { baseVersion, currentVersion, targetVersion } = args;
const { base, current, target } = args;
const diffableBaseVersion = baseVersion ? convertRuleToDiffable(baseVersion) : undefined;
const diffableCurrentVersion = convertRuleToDiffable(currentVersion);
const diffableTargetVersion = convertRuleToDiffable(targetVersion);
invariant(current != null, 'current version is required');
const diffableCurrentVersion = convertRuleToDiffable(current);
invariant(target != null, 'target version is required');
const diffableTargetVersion = convertRuleToDiffable(target);
// Base version is optional
const diffableBaseVersion = base ? convertRuleToDiffable(base) : undefined;
const fieldsDiff = calculateRuleFieldsDiff({
base_version: diffableBaseVersion || MissingVersion,
@ -82,9 +84,9 @@ export const calculateRuleDiff = (args: CalculateRuleDiffArgs): CalculateRuleDif
},
ruleVersions: {
input: {
current: currentVersion,
base: baseVersion,
target: targetVersion,
current,
base,
target,
},
output: {
current: diffableCurrentVersion,

View file

@ -0,0 +1,27 @@
/*
* 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 type { ConfigType } from '../../../../config';
import type { SecuritySolutionApiRequestHandlerContext } from '../../../../types';
import type { IPrebuiltRuleAssetsClient } from './rule_assets/prebuilt_rule_assets_client';
import { installPrebuiltRulesPackage } from '../api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_package';
export async function ensureLatestRulesPackageInstalled(
ruleAssetsClient: IPrebuiltRuleAssetsClient,
config: ConfigType,
securityContext: SecuritySolutionApiRequestHandlerContext
) {
let latestPrebuiltRules = await ruleAssetsClient.fetchLatestAssets();
if (latestPrebuiltRules.length === 0) {
// Seems no packages with prepackaged rules were installed, try to install the default rules package
await installPrebuiltRulesPackage(config, securityContext);
// Try to get the prepackaged rules again
latestPrebuiltRules = await ruleAssetsClient.fetchLatestAssets();
}
return latestPrebuiltRules;
}

View file

@ -14,7 +14,7 @@ import { withSecuritySpan } from '../../../../../utils/with_security_span';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import { validatePrebuiltRuleAssets } from './prebuilt_rule_assets_validation';
import { PREBUILT_RULE_ASSETS_SO_TYPE } from './prebuilt_rule_assets_type';
import type { PrebuiltRuleVersionInfo } from '../../model/rule_versions/prebuilt_rule_version_info';
import type { RuleVersionSpecifier } from '../../model/rule_versions/rule_version_specifier';
const MAX_PREBUILT_RULES_COUNT = 10_000;
const MAX_ASSETS_PER_BULK_CREATE_REQUEST = 500;
@ -22,9 +22,9 @@ const MAX_ASSETS_PER_BULK_CREATE_REQUEST = 500;
export interface IPrebuiltRuleAssetsClient {
fetchLatestAssets: () => Promise<PrebuiltRuleAsset[]>;
fetchLatestVersions(): Promise<PrebuiltRuleVersionInfo[]>;
fetchLatestVersions(): Promise<RuleVersionSpecifier[]>;
fetchAssetsByVersionInfo(versions: PrebuiltRuleVersionInfo[]): Promise<PrebuiltRuleAsset[]>;
fetchAssetsByVersion(versions: RuleVersionSpecifier[]): Promise<PrebuiltRuleAsset[]>;
bulkCreateAssets(assets: PrebuiltRuleAsset[]): Promise<void>;
}
@ -76,7 +76,7 @@ export const createPrebuiltRuleAssetsClient = (
});
},
fetchLatestVersions: (): Promise<PrebuiltRuleVersionInfo[]> => {
fetchLatestVersions: (): Promise<RuleVersionSpecifier[]> => {
return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchLatestVersions', async () => {
const findResult = await savedObjectsClient.find<
PrebuiltRuleAsset,
@ -119,7 +119,7 @@ export const createPrebuiltRuleAssetsClient = (
return buckets.map((bucket) => {
const hit = bucket.latest_version.hits.hits[0];
const soAttributes = hit._source[PREBUILT_RULE_ASSETS_SO_TYPE];
const versionInfo: PrebuiltRuleVersionInfo = {
const versionInfo: RuleVersionSpecifier = {
rule_id: soAttributes.rule_id,
version: soAttributes.version,
};
@ -128,10 +128,8 @@ export const createPrebuiltRuleAssetsClient = (
});
},
fetchAssetsByVersionInfo: (
versions: PrebuiltRuleVersionInfo[]
): Promise<PrebuiltRuleAsset[]> => {
return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchAssetsByVersionInfo', async () => {
fetchAssetsByVersion: (versions: RuleVersionSpecifier[]): Promise<PrebuiltRuleAsset[]> => {
return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchAssetsByVersion', async () => {
if (versions.length === 0) {
// NOTE: without early return it would build incorrect filter and fetch all existing saved objects
return [];

View file

@ -27,7 +27,5 @@ export const createPrebuiltRules = (rulesClient: RulesClient, rules: PrebuiltRul
},
});
if (result.errors.length > 0) {
throw new AggregateError(result.errors, 'Error installing new prebuilt rules');
}
return result;
});

View file

@ -6,43 +6,46 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema';
import type {
RuleResponse,
RuleSignatureId,
} from '../../../../../../common/detection_engine/rule_schema';
import { withSecuritySpan } from '../../../../../utils/with_security_span';
import { findRules } from '../../../rule_management/logic/search/find_rules';
import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules';
import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters';
import type { PrebuiltRuleVersionInfo } from '../../model/rule_versions/prebuilt_rule_version_info';
export interface IPrebuiltRuleObjectsClient {
fetchInstalledRules(): Promise<FetchInstalledRulesResult>;
}
export interface FetchInstalledRulesResult {
installedRules: RuleResponse[];
installedVersions: PrebuiltRuleVersionInfo[];
fetchAllInstalledRules(): Promise<RuleResponse[]>;
fetchInstalledRulesByIds(ruleIds: string[]): Promise<RuleResponse[]>;
}
export const createPrebuiltRuleObjectsClient = (
rulesClient: RulesClient
): IPrebuiltRuleObjectsClient => {
return {
fetchInstalledRules: (): Promise<FetchInstalledRulesResult> => {
fetchAllInstalledRules: (): Promise<RuleResponse[]> => {
return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRules', async () => {
const rulesData = await getExistingPrepackagedRules({ rulesClient });
const rules = rulesData.map((rule) => internalRuleToAPIResponse(rule));
const versions = rules.map((rule) => convertRuleToVersionInfo(rule));
return {
installedRules: rules,
installedVersions: versions,
};
return rules;
});
},
fetchInstalledRulesByIds: (ruleIds: RuleSignatureId[]): Promise<RuleResponse[]> => {
return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRulesByIds', async () => {
const { data } = await findRules({
rulesClient,
perPage: ruleIds.length,
page: 1,
sortField: 'createdAt',
sortOrder: 'desc',
fields: undefined,
filter: `alert.attributes.params.ruleId:(${ruleIds.join(' or ')})`,
});
const rules = data.map((rule) => internalRuleToAPIResponse(rule));
return rules;
});
},
};
};
const convertRuleToVersionInfo = (rule: RuleResponse): PrebuiltRuleVersionInfo => {
const versionInfo: PrebuiltRuleVersionInfo = {
rule_id: rule.rule_id,
version: rule.version,
};
return versionInfo;
};

View file

@ -1,100 +0,0 @@
/*
* 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 { chunk } from 'lodash/fp';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server';
import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions';
import { MAX_RULES_TO_UPDATE_IN_PARALLEL } from '../../../../../../common/constants';
import { createRules } from '../../../rule_management/logic/crud/create_rules';
import { readRules } from '../../../rule_management/logic/crud/read_rules';
import { patchRules } from '../../../rule_management/logic/crud/patch_rules';
import { deleteRules } from '../../../rule_management/logic/crud/delete_rules';
import type { RuleParams } from '../../../rule_schema';
import { PrepackagedRulesError } from '../../api/install_prebuilt_rules_and_timelines/route';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
/**
* Updates existing prebuilt rules given a set of rules and output index.
* This implements a chunked approach to not saturate network connections and
* avoid being a "noisy neighbor".
* @param rulesClient Alerting client
* @param spaceId Current user spaceId
* @param rules The rules to apply the update for
*/
export const updatePrebuiltRules = async (
rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract,
rules: PrebuiltRuleAsset[]
): Promise<void> => {
const ruleChunks = chunk(MAX_RULES_TO_UPDATE_IN_PARALLEL, rules);
for (const ruleChunk of ruleChunks) {
const rulePromises = createPromises(rulesClient, savedObjectsClient, ruleChunk);
await Promise.all(rulePromises);
}
};
/**
* Creates promises of the rules and returns them.
* @param rulesClient Alerting client
* @param spaceId Current user spaceId
* @param rules The rules to apply the update for
* @returns Promise of what was updated.
*/
const createPromises = (
rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract,
rules: PrebuiltRuleAsset[]
): Array<Promise<PartialRule<RuleParams> | null>> => {
return rules.map(async (rule) => {
const existingRule = await readRules({
rulesClient,
ruleId: rule.rule_id,
id: undefined,
});
if (!existingRule) {
throw new PrepackagedRulesError(`Failed to find rule ${rule.rule_id}`, 500);
}
// If we're trying to change the type of a prepackaged rule, we need to delete the old one
// and replace it with the new rule, keeping the enabled setting, actions, throttle, id,
// and exception lists from the old rule
if (rule.type !== existingRule.params.type) {
await deleteRules({
ruleId: existingRule.id,
rulesClient,
});
return createRules({
rulesClient,
params: {
...rule,
// Force the prepackaged rule to use the enabled state from the existing rule,
// regardless of what the prepackaged rule says
enabled: existingRule.enabled,
actions: existingRule.actions.map(transformAlertToRuleAction),
},
});
} else {
return patchRules({
rulesClient,
existingRule,
nextParams: {
...rule,
// Force enabled to use the enabled state from the existing rule by passing in undefined to patchRules
enabled: undefined,
actions: undefined,
},
});
}
});
};

View file

@ -6,12 +6,11 @@
*/
import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import {
getRuleMock,
getFindResultWithSingleHit,
} from '../../../routes/__mocks__/request_responses';
import { updatePrebuiltRules } from './update_prebuilt_rules';
import { upgradePrebuiltRules } from './upgrade_prebuilt_rules';
import { patchRules } from '../../../rule_management/logic/crud/patch_rules';
import { getPrebuiltRuleMock, getPrebuiltThreatMatchRuleMock } from '../../mocks';
import { getThreatRuleParams } from '../../../rule_schema/mocks';
@ -20,11 +19,9 @@ jest.mock('../../../rule_management/logic/crud/patch_rules');
describe('updatePrebuiltRules', () => {
let rulesClient: ReturnType<typeof rulesClientMock.create>;
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
beforeEach(() => {
rulesClient = rulesClientMock.create();
savedObjectsClient = savedObjectsClientMock.create();
});
it('should omit actions and enabled when calling patchRules', async () => {
@ -39,7 +36,7 @@ describe('updatePrebuiltRules', () => {
const prepackagedRule = getPrebuiltRuleMock();
rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
await updatePrebuiltRules(rulesClient, savedObjectsClient, [{ ...prepackagedRule, actions }]);
await upgradePrebuiltRules(rulesClient, [{ ...prepackagedRule, actions }]);
expect(patchRules).toHaveBeenCalledWith(
expect.objectContaining({
@ -70,9 +67,7 @@ describe('updatePrebuiltRules', () => {
data: [getRuleMock(getThreatRuleParams())],
});
await updatePrebuiltRules(rulesClient, savedObjectsClient, [
{ ...prepackagedRule, ...updatedThreatParams },
]);
await upgradePrebuiltRules(rulesClient, [{ ...prepackagedRule, ...updatedThreatParams }]);
expect(patchRules).toHaveBeenCalledWith(
expect.objectContaining({

View file

@ -0,0 +1,106 @@
/*
* 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 type { SanitizedRule } from '@kbn/alerting-plugin/common';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import { MAX_RULES_TO_UPDATE_IN_PARALLEL } from '../../../../../../common/constants';
import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions';
import { initPromisePool } from '../../../../../utils/promise_pool';
import { withSecuritySpan } from '../../../../../utils/with_security_span';
import { createRules } from '../../../rule_management/logic/crud/create_rules';
import { deleteRules } from '../../../rule_management/logic/crud/delete_rules';
import { patchRules } from '../../../rule_management/logic/crud/patch_rules';
import { readRules } from '../../../rule_management/logic/crud/read_rules';
import type { RuleParams } from '../../../rule_schema';
import { PrepackagedRulesError } from '../../api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
/**
* Upgrades existing prebuilt rules given a set of rules and output index.
* This implements a chunked approach to not saturate network connections and
* avoid being a "noisy neighbor".
* @param rulesClient Alerting client
* @param rules The rules to apply the update for
*/
export const upgradePrebuiltRules = async (rulesClient: RulesClient, rules: PrebuiltRuleAsset[]) =>
withSecuritySpan('upgradePrebuiltRules', async () => {
const result = await initPromisePool({
concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL,
items: rules,
executor: async (rule) => {
return upgradeRule(rulesClient, rule);
},
});
return result;
});
/**
* Upgrades a rule
*
* @param rulesClient Alerting client
* @param rule The rule to apply the update for
* @returns Promise of what was updated.
*/
const upgradeRule = async (
rulesClient: RulesClient,
rule: PrebuiltRuleAsset
): Promise<SanitizedRule<RuleParams>> => {
const existingRule = await readRules({
rulesClient,
ruleId: rule.rule_id,
id: undefined,
});
if (!existingRule) {
throw new PrepackagedRulesError(`Failed to find rule ${rule.rule_id}`, 500);
}
// If we're trying to change the type of a prepackaged rule, we need to delete the old one
// and replace it with the new rule, keeping the enabled setting, actions, throttle, id,
// and exception lists from the old rule
if (rule.type !== existingRule.params.type) {
await deleteRules({
ruleId: existingRule.id,
rulesClient,
});
return createRules({
rulesClient,
params: {
...rule,
// Force the prepackaged rule to use the enabled state from the existing rule,
// regardless of what the prepackaged rule says
enabled: existingRule.enabled,
actions: existingRule.actions.map(transformAlertToRuleAction),
},
});
} else {
await patchRules({
rulesClient,
existingRule,
nextParams: {
...rule,
// Force enabled to use the enabled state from the existing rule by passing in undefined to patchRules
enabled: undefined,
actions: undefined,
},
});
const updatedRule = await readRules({
rulesClient,
ruleId: rule.rule_id,
id: undefined,
});
if (!updatedRule) {
throw new PrepackagedRulesError(`Rule ${rule.rule_id} not found after upgrade`, 500);
}
return updatedRule;
}
};

View file

@ -0,0 +1,37 @@
/*
* 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 type { RuleVersions } from '../diff/calculate_rule_diff';
import type { IPrebuiltRuleAssetsClient } from '../rule_assets/prebuilt_rule_assets_client';
import type { IPrebuiltRuleObjectsClient } from '../rule_objects/prebuilt_rule_objects_client';
import type { RuleVersionSpecifier } from '../../model/rule_versions/rule_version_specifier';
import { zipRuleVersions } from './zip_rule_versions';
interface GetRuleVersionsMapArgs {
ruleObjectsClient: IPrebuiltRuleObjectsClient;
ruleAssetsClient: IPrebuiltRuleAssetsClient;
versionSpecifiers?: RuleVersionSpecifier[];
}
export async function fetchRuleVersionsTriad({
ruleObjectsClient,
ruleAssetsClient,
versionSpecifiers,
}: GetRuleVersionsMapArgs): Promise<Map<string, RuleVersions>> {
const [currentRules, latestRules] = await Promise.all([
versionSpecifiers
? ruleObjectsClient.fetchInstalledRulesByIds(
versionSpecifiers.map(({ rule_id: ruleId }) => ruleId)
)
: ruleObjectsClient.fetchAllInstalledRules(),
versionSpecifiers
? ruleAssetsClient.fetchAssetsByVersion(versionSpecifiers)
: ruleAssetsClient.fetchLatestAssets(),
]);
const baseRules = await ruleAssetsClient.fetchAssetsByVersion(currentRules);
return zipRuleVersions(currentRules, baseRules, latestRules);
}

View file

@ -0,0 +1,43 @@
/*
* 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 type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import type { RuleVersions } from '../diff/calculate_rule_diff';
export const zipRuleVersions = (
installedRules: RuleResponse[],
baseRules: PrebuiltRuleAsset[],
latestRules: PrebuiltRuleAsset[]
): Map<string, RuleVersions> => {
const baseRulesMap = new Map(baseRules.map((r) => [r.rule_id, r]));
const latestRulesMap = new Map(latestRules.map((r) => [r.rule_id, r]));
const currentRulesMap = new Map(installedRules.map((r) => [r.rule_id, r]));
const uniqueRuleIds = new Set([
...Array.from(baseRulesMap.keys()),
...Array.from(latestRulesMap.keys()),
...Array.from(currentRulesMap.keys()),
]);
return new Map(
[...uniqueRuleIds].map((ruleId) => {
const base = baseRulesMap.get(ruleId);
const target = latestRulesMap.get(ruleId);
const current = currentRulesMap.get(ruleId);
return [
ruleId,
{
current,
base,
target,
},
];
})
);
};

View file

@ -5,50 +5,62 @@
* 2.0.
*/
import type { PrebuiltRuleVersionInfo } from './prebuilt_rule_version_info';
export interface GetVersionBucketsArgs {
latestVersions: PrebuiltRuleVersionInfo[];
installedVersions: PrebuiltRuleVersionInfo[];
}
import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema';
import type { RuleVersions } from '../../logic/diff/calculate_rule_diff';
import type { PrebuiltRuleAsset } from '../rule_assets/prebuilt_rule_asset';
export interface VersionBuckets {
latestVersions: PrebuiltRuleVersionInfo[];
installedVersions: PrebuiltRuleVersionInfo[];
latestVersionsToInstall: PrebuiltRuleVersionInfo[];
latestVersionsToUpgrade: PrebuiltRuleVersionInfo[];
installedVersionsToUpgrade: PrebuiltRuleVersionInfo[];
/**
* Rules that are currently installed in Kibana
*/
currentRules: RuleResponse[];
/**
* Rules that are ready to be installed
*/
installableRules: PrebuiltRuleAsset[];
/**
* Rules that are installed but outdated
*/
upgradeableRules: Array<{
/**
* The currently installed version
*/
current: RuleResponse;
/**
* The latest available version
*/
target: PrebuiltRuleAsset;
}>;
}
export const getVersionBuckets = (args: GetVersionBucketsArgs): VersionBuckets => {
const { latestVersions, installedVersions } = args;
export const getVersionBuckets = (ruleVersionsMap: Map<string, RuleVersions>): VersionBuckets => {
const currentRules: RuleResponse[] = [];
const installableRules: PrebuiltRuleAsset[] = [];
const upgradeableRules: VersionBuckets['upgradeableRules'] = [];
const installedVersionsMap = new Map(installedVersions.map((item) => [item.rule_id, item]));
const latestVersionsToInstall: PrebuiltRuleVersionInfo[] = [];
const latestVersionsToUpgrade: PrebuiltRuleVersionInfo[] = [];
const installedVersionsToUpgrade: PrebuiltRuleVersionInfo[] = [];
latestVersions.forEach((latestVersion) => {
const installedVersion = installedVersionsMap.get(latestVersion.rule_id);
if (installedVersion == null) {
// If this rule is not installed
latestVersionsToInstall.push(latestVersion);
ruleVersionsMap.forEach(({ current, target }) => {
if (current != null) {
// If this rule is installed
currentRules.push(current);
}
if (installedVersion != null && installedVersion.version < latestVersion.version) {
if (current == null && target != null) {
// If this rule is not installed
installableRules.push(target);
}
if (current != null && target != null && current.version < target.version) {
// If this rule is installed but outdated
latestVersionsToUpgrade.push(latestVersion);
installedVersionsToUpgrade.push(installedVersion);
upgradeableRules.push({
current,
target,
});
}
});
return {
latestVersions,
installedVersions,
latestVersionsToInstall,
latestVersionsToUpgrade,
installedVersionsToUpgrade,
currentRules,
installableRules,
upgradeableRules,
};
};

View file

@ -10,7 +10,7 @@ import type {
RuleVersion,
} from '../../../../../../common/detection_engine/rule_schema';
export interface PrebuiltRuleVersionInfo {
export interface RuleVersionSpecifier {
rule_id: RuleSignatureId;
version: RuleVersion;
}

View file

@ -48,16 +48,6 @@ describe('get_existing_prepackaged_rules', () => {
result3.params.immutable = true;
result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a';
// first result mock which is for returning the total
rulesClient.find.mockResolvedValueOnce(
getFindResultWithMultiHits({
data: [result1],
perPage: 1,
page: 1,
total: 3,
})
);
// second mock which will return all the data on a single page
rulesClient.find.mockResolvedValueOnce(
getFindResultWithMultiHits({
@ -90,16 +80,6 @@ describe('get_existing_prepackaged_rules', () => {
const result2 = getRuleMock(getQueryRuleParams());
result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d';
// first result mock which is for returning the total
rulesClient.find.mockResolvedValueOnce(
getFindResultWithMultiHits({
data: [result1],
perPage: 1,
page: 1,
total: 2,
})
);
// second mock which will return all the data on a single page
rulesClient.find.mockResolvedValueOnce(
getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 })
@ -121,16 +101,6 @@ describe('get_existing_prepackaged_rules', () => {
const result3 = getRuleMock(getQueryRuleParams());
result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a';
// first result mock which is for returning the total
rulesClient.find.mockResolvedValueOnce(
getFindResultWithMultiHits({
data: [result1],
perPage: 3,
page: 1,
total: 3,
})
);
// second mock which will return all the data on a single page
rulesClient.find.mockResolvedValueOnce(
getFindResultWithMultiHits({
@ -163,16 +133,6 @@ describe('get_existing_prepackaged_rules', () => {
const result2 = getRuleMock(getQueryRuleParams());
result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d';
// first result mock which is for returning the total
rulesClient.find.mockResolvedValueOnce(
getFindResultWithMultiHits({
data: [result1],
perPage: 1,
page: 1,
total: 2,
})
);
// second mock which will return all the data on a single page
rulesClient.find.mockResolvedValueOnce(
getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 })

View file

@ -10,6 +10,7 @@ import { withSecuritySpan } from '../../../../../utils/with_security_span';
import { findRules } from './find_rules';
import type { RuleAlertType } from '../../../rule_schema';
export const MAX_PREBUILT_RULES_COUNT = 10_000;
export const FILTER_NON_PREPACKED_RULES = 'alert.attributes.params.immutable: false';
export const FILTER_PREPACKED_RULES = 'alert.attributes.params.immutable: true';
@ -50,11 +51,10 @@ export const getRules = async ({
filter: string;
}): Promise<RuleAlertType[]> =>
withSecuritySpan('getRules', async () => {
const count = await getRulesCount({ rulesClient, filter });
const rules = await findRules({
rulesClient,
filter,
perPage: count,
perPage: MAX_PREBUILT_RULES_COUNT,
page: 1,
sortField: 'createdAt',
sortOrder: 'desc',

View file

@ -0,0 +1,38 @@
/*
* 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.
*/
/**
* Extracts error message from an error
*
* @param error Unknown error
* @returns error message
*/
export const getErrorMessage = (error: unknown) => {
if (error instanceof Error) {
return error.message;
} else if (typeof error === 'string') {
return error;
} else {
return 'Unknown error';
}
};
const hasStatusCode = (error: unknown): error is { statusCode: unknown } =>
typeof error === 'object' && error !== null && 'statusCode' in error;
/**
* Extracts status code from an error
*
* @param error Unknown error
* @returns Stats code if it exists
*/
export const getErrorStatusCode = (error: unknown): number | undefined => {
if (hasStatusCode(error)) {
return Number(error.statusCode);
}
return undefined;
};