mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
33f5bb6ba5
commit
74d276ed0c
36 changed files with 967 additions and 420 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
>;
|
|
@ -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[];
|
||||
}
|
|
@ -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>;
|
|
@ -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[];
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}>;
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getPrebuiltRulesAndTimelinesStatusRoute } from './route';
|
||||
import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status_route';
|
||||
|
||||
import {
|
||||
getEmptyFindResult,
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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';
|
|
@ -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,
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 });
|
|
@ -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,
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 [];
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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({
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
|||
RuleVersion,
|
||||
} from '../../../../../../common/detection_engine/rule_schema';
|
||||
|
||||
export interface PrebuiltRuleVersionInfo {
|
||||
export interface RuleVersionSpecifier {
|
||||
rule_id: RuleSignatureId;
|
||||
version: RuleVersion;
|
||||
}
|
|
@ -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 })
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue