From 8f3555b2b3228567c3ba738f7a987ca803b60d24 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Wed, 19 Feb 2025 17:54:43 -0500 Subject: [PATCH 01/18] switches diff algorithm logic --- .../common/prebuilt_rule_filter.ts | 33 ++++ .../detection_engine/prebuilt_rules/index.ts | 1 + .../diff/three_way_diff/three_way_diff.ts | 3 +- .../review_rule_upgrade_route.ts | 65 +++++++- .../rule_management/rule_fields.ts | 1 + .../rule_management/rule_filtering.ts | 13 +- .../rule_management/api/api.ts | 25 ++- .../use_fetch_prebuilt_rules_status_query.ts | 12 +- ...tch_prebuilt_rules_upgrade_review_query.ts | 10 +- .../create_upgradeable_rules_payload.ts | 28 ++-- .../review_rule_upgrade_route.ts | 12 +- .../logic/diff/calculate_rule_diff.ts | 19 ++- .../algorithms/data_source_diff_algorithm.ts | 31 ++-- .../algorithms/eql_query_diff_algorithm.ts | 6 +- .../algorithms/esql_query_diff_algorithm.ts | 6 +- .../algorithms/kql_query_diff_algorithm.ts | 5 +- .../multi_line_string_diff_algorithm.test.ts | 138 +++++++++++------ .../multi_line_string_diff_algorithm.ts | 29 +++- .../algorithms/number_diff_algorithm.ts | 5 +- .../algorithms/rule_type_diff_algorithm.ts | 25 ++- .../scalar_array_diff_algorithm.test.ts | 144 ++++++++++++------ .../algorithms/scalar_array_diff_algorithm.ts | 39 +++-- .../algorithms/simple_diff_algorithm.ts | 32 ++-- .../single_line_string_diff_algorithm.ts | 5 +- .../calculation/calculate_rule_fields_diff.ts | 112 +++++++++----- .../calculation/diff_calculation_helpers.ts | 5 +- 26 files changed, 564 insertions(+), 240 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rule_filter.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rule_filter.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rule_filter.ts new file mode 100644 index 0000000000000..255af69c89cda --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rule_filter.ts @@ -0,0 +1,33 @@ +/* + * 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 { z } from '@kbn/zod'; + +export enum RuleCustomizationStatus { + CUSTOMIZED = 'CUSTOMIZED', + NOT_CUSTOMIZED = 'NOT_CUSTOMIZED', +} + +export type PrebuiltRuleFilter = z.infer; +export const PrebuiltRuleFilter = z.object({ + /** + * Rule IDs to return upgrade info for + */ + rule_ids: z.array(z.string()).optional(), + /** + * Tags to filter by + */ + tags: z.array(z.string()).optional(), + /** + * Rule name to filter by + */ + name: z.string().optional(), + /** + * Rule customization status to filter by + */ + customization_status: z.nativeEnum(RuleCustomizationStatus).optional(), +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts index 02ab5c8a3cc0c..ab7cff0f8b9fb 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts @@ -22,3 +22,4 @@ export * from './model/diff/three_way_diff/three_way_diff_outcome'; export * from './model/diff/three_way_diff/three_way_diff'; export * from './model/diff/three_way_diff/three_way_diff_conflict'; export * from './model/diff/three_way_diff/three_way_merge_outcome'; +export * from './common/prebuilt_rule_filter'; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff.ts index c8c4238709849..c85d5e5824eae 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff.ts @@ -145,5 +145,6 @@ export interface ThreeWayDiff { * Given the three versions of a value, calculates a three-way diff for it. */ export type ThreeWayDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ) => ThreeWayDiff; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts index 2f2d6e3bd1c26..02bb7e55aa9b4 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts @@ -5,35 +5,86 @@ * 2.0. */ -import type { RuleObjectId, RuleSignatureId, RuleTagArray } from '../../model'; +import { z } from '@kbn/zod'; +import { SortOrder, type RuleObjectId, type RuleSignatureId, type RuleTagArray } from '../../model'; import type { PartialRuleDiff } from '../model'; -import type { RuleResponse } from '../../model/rule_schema'; +import type { RuleResponse, RuleVersion } from '../../model/rule_schema'; +import { FindRulesSortField } from '../../rule_management'; +import { PrebuiltRuleFilter } from '../common/prebuilt_rule_filter'; + +export type ReviewRuleUpgradeSort = z.infer; +export const ReviewRuleUpgradeSort = z.object({ + /** + * Field to sort by + */ + field: FindRulesSortField.optional(), + /** + * Sort order + */ + order: SortOrder.optional(), +}); + +export type ReviewRuleUpgradeRequestBody = z.infer; +export const ReviewRuleUpgradeRequestBody = z + .object({ + filter: PrebuiltRuleFilter.optional(), + sort: ReviewRuleUpgradeSort.optional(), + + page: z.coerce.number().int().min(1).optional().default(1), + /** + * Rules per page + */ + per_page: z.coerce.number().int().min(0).optional().default(20), + }) + .nullable(); export interface ReviewRuleUpgradeResponseBody { - /** Aggregated info about all rules available for upgrade */ + /** + * @deprecated Use the prebuilt rule status API instead. The field is kept + * here for backward compatibility but can be removed after one Serverless + * release. + */ stats: RuleUpgradeStatsForReview; /** Info about individual rules: one object per each rule available for upgrade */ rules: RuleUpgradeInfoForReview[]; + + /** The requested page number */ + page: number; + + /** The requested number of items per page */ + per_page: number; + + /** The total number of rules available for upgrade that match the filter criteria */ + total: number; } export interface RuleUpgradeStatsForReview { - /** Number of installed prebuilt rules available for upgrade (stock + customized) */ + /** + * @deprecated Always 0 + */ num_rules_to_upgrade_total: number; - /** Number of installed prebuilt rules with upgrade conflicts (SOLVABLE or NON_SOLVABLE) */ + /** + * @deprecated Always 0 + */ num_rules_with_conflicts: number; - /** Number of installed prebuilt rules with NON_SOLVABLE upgrade conflicts */ + /** + * @deprecated Always 0 + */ num_rules_with_non_solvable_conflicts: number; - /** A union of all tags of all rules available for upgrade */ + /** + * @deprecated Always an empty array + */ tags: RuleTagArray; } export interface RuleUpgradeInfoForReview { id: RuleObjectId; rule_id: RuleSignatureId; + version: RuleVersion; current_rule: RuleResponse; target_rule: RuleResponse; diff: PartialRuleDiff; diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_fields.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_fields.ts index 5d72cd15a96ae..610b2231e8ee7 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_fields.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_fields.ts @@ -22,3 +22,4 @@ export const TAGS_FIELD = 'alert.attributes.tags'; export const PARAMS_TYPE_FIELD = 'alert.attributes.params.type'; export const PARAMS_IMMUTABLE_FIELD = 'alert.attributes.params.immutable'; export const LAST_RUN_OUTCOME_FIELD = 'alert.attributes.lastRun.outcome'; +export const IS_CUSTOMIZED_FIELD = 'alert.attributes.params.ruleSource.isCustomized'; diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts index 52ace0cfac5da..692f2fa55a5e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts @@ -7,10 +7,11 @@ import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { RuleExecutionStatus } from '../../api/detection_engine'; -import { RuleExecutionStatusEnum } from '../../api/detection_engine'; +import { RuleCustomizationStatus, RuleExecutionStatusEnum } from '../../api/detection_engine'; import { prepareKQLStringParam } from '../../utils/kql'; import { ENABLED_FIELD, + IS_CUSTOMIZED_FIELD, LAST_RUN_OUTCOME_FIELD, PARAMS_IMMUTABLE_FIELD, PARAMS_TYPE_FIELD, @@ -23,6 +24,8 @@ export const KQL_FILTER_IMMUTABLE_RULES = `${PARAMS_IMMUTABLE_FIELD}: true`; export const KQL_FILTER_MUTABLE_RULES = `${PARAMS_IMMUTABLE_FIELD}: false`; export const KQL_FILTER_ENABLED_RULES = `${ENABLED_FIELD}: true`; export const KQL_FILTER_DISABLED_RULES = `${ENABLED_FIELD}: false`; +export const KQL_FILTER_CUSTOMIZED_RULES = `${IS_CUSTOMIZED_FIELD}: true`; +export const KQL_FILTER_NOT_CUSTOMIZED_RULES = `${IS_CUSTOMIZED_FIELD}: false`; interface RulesFilterOptions { filter: string; @@ -32,6 +35,7 @@ interface RulesFilterOptions { tags: string[]; excludeRuleTypes: Type[]; ruleExecutionStatus: RuleExecutionStatus; + customizationStatus: RuleCustomizationStatus; ruleIds: string[]; } @@ -50,6 +54,7 @@ export function convertRulesFilterToKQL({ tags, excludeRuleTypes = [], ruleExecutionStatus, + customizationStatus, }: Partial): string { const kql: string[] = []; @@ -85,6 +90,12 @@ export function convertRulesFilterToKQL({ kql.push(`${LAST_RUN_OUTCOME_FIELD}: "failed"`); } + if (customizationStatus === RuleCustomizationStatus.CUSTOMIZED) { + kql.push(KQL_FILTER_CUSTOMIZED_RULES); + } else if (customizationStatus === RuleCustomizationStatus.NOT_CUSTOMIZED) { + kql.push(KQL_FILTER_NOT_CUSTOMIZED_RULES); + } + return kql.join(' AND '); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 5f5fded87ac45..400a18a8103b9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -15,14 +15,14 @@ import type { ActionType, AsApiContract } from '@kbn/actions-plugin/common'; import type { ActionResult } from '@kbn/actions-plugin/server'; import { convertRulesFilterToKQL } from '../../../../common/detection_engine/rule_management/rule_filtering'; import type { - UpgradeSpecificRulesRequest, - PickVersionValues, PerformRuleUpgradeResponseBody, InstallSpecificRulesRequest, PerformRuleInstallationResponseBody, GetPrebuiltRulesStatusResponseBody, ReviewRuleUpgradeResponseBody, ReviewRuleInstallationResponseBody, + ReviewRuleUpgradeRequestBody, + PerformRuleUpgradeRequestBody, } from '../../../../common/api/detection_engine/prebuilt_rules'; import type { BulkDuplicateRules, @@ -637,13 +637,16 @@ export const getPrebuiltRulesStatus = async ({ */ export const reviewRuleUpgrade = async ({ signal, + request, }: { signal: AbortSignal | undefined; + request: ReviewRuleUpgradeRequestBody; }): Promise => KibanaServices.get().http.fetch(REVIEW_RULE_UPGRADE_URL, { method: 'POST', version: '1', signal, + body: JSON.stringify(request), }); /** @@ -685,23 +688,13 @@ export const performInstallSpecificRules = async ( }), }); -export interface PerformUpgradeRequest { - rules: UpgradeSpecificRulesRequest['rules']; - pickVersion: PickVersionValues; -} - -export const performUpgradeSpecificRules = async ({ - rules, - pickVersion, -}: PerformUpgradeRequest): Promise => +export const performUpgradeRules = async ( + body: PerformRuleUpgradeRequestBody +): Promise => KibanaServices.get().http.fetch(PERFORM_RULE_UPGRADE_URL, { method: 'POST', version: '1', - body: JSON.stringify({ - mode: 'SPECIFIC_RULES', - rules, - pick_version: pickVersion, - }), + body: JSON.stringify(body), }); export const bootstrapPrebuiltRules = async (): Promise => diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts index 0c0515e61b818..376877326a5e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts @@ -4,24 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useCallback } from 'react'; import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import type { PrebuiltRulesStatusStats } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { useCallback } from 'react'; +import type { GetPrebuiltRulesStatusResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { GET_PREBUILT_RULES_STATUS_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { getPrebuiltRulesStatus } from '../../api'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; -import { GET_PREBUILT_RULES_STATUS_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; export const PREBUILT_RULES_STATUS_QUERY_KEY = ['GET', GET_PREBUILT_RULES_STATUS_URL]; export const useFetchPrebuiltRulesStatusQuery = ( - options?: UseQueryOptions + options?: UseQueryOptions ) => { - return useQuery( + return useQuery( PREBUILT_RULES_STATUS_QUERY_KEY, async ({ signal }) => { const response = await getPrebuiltRulesStatus({ signal }); - return response.stats; + return response; }, { ...DEFAULT_QUERY_OPTIONS, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts index 532114b1d4b62..4b779918febc3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts @@ -9,7 +9,10 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { reviewRuleUpgrade } from '../../api'; import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; -import type { ReviewRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { + ReviewRuleUpgradeRequestBody, + ReviewRuleUpgradeResponseBody, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; import { cappedExponentialBackoff } from './capped_exponential_backoff'; @@ -17,12 +20,13 @@ import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const REVIEW_RULE_UPGRADE_QUERY_KEY = ['POST', REVIEW_RULE_UPGRADE_URL]; export const useFetchPrebuiltRulesUpgradeReviewQuery = ( + request: ReviewRuleUpgradeRequestBody, options?: UseQueryOptions ) => { return useQuery( - REVIEW_RULE_UPGRADE_QUERY_KEY, + [...REVIEW_RULE_UPGRADE_QUERY_KEY, request], async ({ signal }) => { - const response = await reviewRuleUpgrade({ signal }); + const response = await reviewRuleUpgrade({ signal, request }); return response; }, { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts index 354cc1257f776..21c1680cd85e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts @@ -58,17 +58,23 @@ export const createModifiedPrebuiltRuleAssets = ({ assertPickVersionIsTarget({ ruleId, requestBody }); } - const calculatedRuleDiff = calculateRuleFieldsDiff({ - base_version: upgradeableRule.base - ? convertRuleToDiffable( - convertPrebuiltRuleAssetToRuleResponse(upgradeableRule.base) - ) - : MissingVersion, - current_version: convertRuleToDiffable(upgradeableRule.current), - target_version: convertRuleToDiffable( - convertPrebuiltRuleAssetToRuleResponse(upgradeableRule.target) - ), - }) as AllFieldsDiff; + const isRuleCustomized = + current.rule_source.type === 'external' && current.rule_source.is_customized === true; + + const calculatedRuleDiff = calculateRuleFieldsDiff( + { + base_version: upgradeableRule.base + ? convertRuleToDiffable( + convertPrebuiltRuleAssetToRuleResponse(upgradeableRule.base) + ) + : MissingVersion, + current_version: convertRuleToDiffable(upgradeableRule.current), + target_version: convertRuleToDiffable( + convertPrebuiltRuleAssetToRuleResponse(upgradeableRule.target) + ), + }, + isRuleCustomized + ) as AllFieldsDiff; if (mode === 'ALL_RULES' && globalPickVersion === 'MERGED') { const fieldsWithConflicts = Object.keys(getFieldsDiffConflicts(calculatedRuleDiff)); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index 54f51752b7ab8..94ee1282fae98 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { + REVIEW_RULE_UPGRADE_URL, + ReviewRuleUpgradeRequestBody, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; import { @@ -34,7 +38,11 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => .addVersion( { version: '1', - validate: {}, + validate: { + request: { + body: buildRouteValidationWithZod(ReviewRuleUpgradeRequestBody), + }, + }, }, reviewRuleUpgradeHandler ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts index ed4f142d20596..5fb266f06427c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts @@ -66,9 +66,9 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult = const { base, current, target } = args; invariant(current != null, 'current version is required'); - const diffableCurrentVersion = convertRuleToDiffable( - convertPrebuiltRuleAssetToRuleResponse(current) - ); + const diffableCurrentVersion = convertRuleToDiffable(current); + const isRuleCustomized = + current.rule_source.type === 'external' && current.rule_source.is_customized === true; invariant(target != null, 'target version is required'); const diffableTargetVersion = convertRuleToDiffable( @@ -80,11 +80,14 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult = ? convertRuleToDiffable(convertPrebuiltRuleAssetToRuleResponse(base)) : undefined; - const fieldsDiff = calculateRuleFieldsDiff({ - base_version: diffableBaseVersion || MissingVersion, - current_version: diffableCurrentVersion, - target_version: diffableTargetVersion, - }); + const fieldsDiff = calculateRuleFieldsDiff( + { + base_version: diffableBaseVersion || MissingVersion, + current_version: diffableCurrentVersion, + target_version: diffableTargetVersion, + }, + isRuleCustomized + ); const { numberFieldsWithUpdates, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts index 86aa886592468..368025d612dca 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts @@ -27,7 +27,8 @@ import { getDedupedDataSourceVersion, mergeDedupedArrays } from './helpers'; * Takes a type of `RuleDataSource | undefined` because the data source can be index patterns, a data view id, or neither in some cases */ export const dataSourceDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -46,6 +47,7 @@ export const dataSourceDiffAlgorithm = ( currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -73,6 +75,7 @@ interface MergeArgs { currentVersion: RuleDataSource | undefined; targetVersion: RuleDataSource | undefined; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ @@ -80,6 +83,7 @@ const mergeVersions = ({ currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { const dedupedBaseVersion = baseVersion ? getDedupedDataSourceVersion(baseVersion) : baseVersion; const dedupedCurrentVersion = currentVersion @@ -90,9 +94,6 @@ const mergeVersions = ({ : targetVersion; switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: @@ -140,14 +141,26 @@ const mergeVersions = ({ }; } - // Scenario -AB is treated as scenario ABC, but marked as - // SOLVABLE, and returns the target version as the merged version - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { + return { + conflict: ThreeWayDiffConflict.NONE, + mergedVersion: dedupedTargetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + }; + } + + // Missing base versions always return target version + // If the rule is customized, we return a SOLVABLE conflict + // Otherwise we treat scenario -AB as AAB + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { - mergedVersion: targetVersion, + conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE, + mergedVersion: dedupedTargetVersion, mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, }; } default: diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.ts index fa3e87397a2a7..c29e7d3358c92 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.ts @@ -14,5 +14,7 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm'; /** * Diff algorithm for eql query types */ -export const eqlQueryDiffAlgorithm = (versions: ThreeVersionsOf) => - simpleDiffAlgorithm(versions); +export const eqlQueryDiffAlgorithm = ( + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.ts index 8360800ad4676..1aa0496041d67 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.ts @@ -14,5 +14,7 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm'; /** * Diff algorithm for esql query types */ -export const esqlQueryDiffAlgorithm = (versions: ThreeVersionsOf) => - simpleDiffAlgorithm(versions); +export const esqlQueryDiffAlgorithm = ( + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.ts index 500c3211cc9c2..4f0a61b479d79 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.ts @@ -15,5 +15,6 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm'; * Diff algorithm for all kql query types (`inline_query` and `saved_query`) */ export const kqlQueryDiffAlgorithm = ( - versions: ThreeVersionsOf -) => simpleDiffAlgorithm(versions); + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.test.ts index 4ecb828a77d4c..532c24cb1caa4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.test.ts @@ -32,7 +32,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_A, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -51,7 +51,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_A, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -70,7 +70,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_B, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -89,7 +89,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_B, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -109,7 +109,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_C, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -130,7 +130,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: 'My description.\nThis is a MODIFIED second line.', }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -151,7 +151,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: 'My EXCELLENT description.\nThis is a second line.', }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -171,7 +171,7 @@ describe('multiLineStringDiffAlgorithm', () => { }; const startTime = performance.now(); - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); const endTime = performance.now(); // If the regex merge in this function takes over 2 sec, this test fails @@ -192,46 +192,92 @@ describe('multiLineStringDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: TEXT_M_A, - target_version: TEXT_M_A, - }; - - const result = multiLineStringDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if target_version as merged output if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is not customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: TEXT_M_A, + target_version: TEXT_M_A, + }; + + const result = multiLineStringDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: TEXT_M_A, + target_version: TEXT_M_A, + }; + + const result = multiLineStringDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: TEXT_M_A, - target_version: TEXT_M_B, - }; - - const result = multiLineStringDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('returns NONE conflict if current_version and target_version are different and rule is not customized - scenario -AB', () => { + it('returns NONE conflict if rule is not customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: TEXT_M_A, + target_version: TEXT_M_B, + }; + + const result = multiLineStringDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: TEXT_M_A, + target_version: TEXT_M_B, + }; + + const result = multiLineStringDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts index e09d8e110bff0..2b788ad36704a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts @@ -24,7 +24,8 @@ import { * Diff algorithm used for string fields that contain multiple lines */ export const multiLineStringDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -42,6 +43,7 @@ export const multiLineStringDiffAlgorithm = ( currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -69,6 +71,7 @@ interface MergeArgs { currentVersion: string; targetVersion: string; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ @@ -76,11 +79,9 @@ const mergeVersions = ({ currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: @@ -118,14 +119,26 @@ const mergeVersions = ({ }; } - // Scenario -AB is treated as scenario ABC, but marked as - // SOLVABLE, and returns the target version as the merged version - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { + return { + conflict: ThreeWayDiffConflict.NONE, + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + }; + } + + // Missing base versions always return target version + // If the rule is customized, we return a SOLVABLE conflict + // Otherwise we treat scenario -AB as AAB + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { + conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE, mergedVersion: targetVersion, mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, }; } default: diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts index 30c32a475ecfb..7737f8d143e5d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts @@ -9,5 +9,6 @@ import type { ThreeVersionsOf } from '../../../../../../../../common/api/detecti import { simpleDiffAlgorithm } from './simple_diff_algorithm'; export const numberDiffAlgorithm = ( - versions: ThreeVersionsOf -) => simpleDiffAlgorithm(versions); + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts index 0701d1e46d251..f27a99a7b43e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts @@ -21,7 +21,8 @@ import { } from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; export const ruleTypeDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -37,6 +38,7 @@ export const ruleTypeDiffAlgorithm = ( const { mergeOutcome, conflict, mergedVersion } = mergeVersions({ targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -62,16 +64,15 @@ interface MergeResult { interface MergeArgs { targetVersion: TValue; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: return { conflict: ThreeWayDiffConflict.NONE, @@ -83,8 +84,9 @@ const mergeVersions = ({ case ThreeWayDiffOutcome.StockValueCanUpdate: // NOTE: This scenario is currently inaccessible via normal UI or API workflows, but the logic is covered just in case case ThreeWayDiffOutcome.CustomizedValueCanUpdate: - // Scenario -AB is treated as scenario ABC: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // We return all -AB rule type fields as NON_SOLVABLE, whether or not the rule is customized + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { mergedVersion: targetVersion, @@ -92,6 +94,17 @@ const mergeVersions = ({ conflict: ThreeWayDiffConflict.NON_SOLVABLE, }; } + + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { + return { + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }; + } default: return assertUnreachable(diffOutcome); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.test.ts index f041aa139bf41..f420d84c8473f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.test.ts @@ -22,7 +22,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'two', 'three'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -41,7 +41,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'two', 'three'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -60,7 +60,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'four', 'three'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -79,7 +79,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'four', 'three'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -99,7 +99,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['three', 'four', 'five', 'six']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -112,46 +112,94 @@ describe('scalarArrayDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: ['one', 'two', 'three'], - target_version: ['one', 'two', 'three'], - }; + describe('returns target_version as merged output if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is not customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: ['one', 'two', 'three'], + target_version: ['one', 'two', 'three'], + }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: ['one', 'two', 'three'], + target_version: ['one', 'two', 'three'], + }; + + const result = scalarArrayDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: ['one', 'two', 'three'], - target_version: ['one', 'four', 'three'], - }; + describe('if current_version and target_version are different - scenario -AB', () => { + it('returns target_version as merged output and NONE conflict if rule is not customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: ['one', 'two', 'three'], + target_version: ['one', 'four', 'three'], + }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns merged version of current and target as merged output if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: ['one', 'two', 'three'], + target_version: ['one', 'four', 'three'], + }; + + const expectedMergedVersion = ['one', 'two', 'three', 'four']; + + const result = scalarArrayDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: expectedMergedVersion, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Merged, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); @@ -163,7 +211,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['three', 'one', 'two'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -184,7 +232,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['one', 'two']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -204,7 +252,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['one', 'two']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -224,7 +272,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['one', 'two']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -244,7 +292,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['three']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -265,7 +313,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'two'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -284,7 +332,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'two'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -303,7 +351,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: [], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -322,7 +370,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: [], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts index 215e92377a596..92ceadc50576e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { uniq } from 'lodash'; +import { union, uniq } from 'lodash'; import { assertUnreachable } from '../../../../../../../../common/utility_types'; import type { ThreeVersionsOf, @@ -27,7 +27,8 @@ import { mergeDedupedArrays } from './helpers'; * NOTE: Diffing logic will be agnostic to array order */ export const scalarArrayDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -45,6 +46,7 @@ export const scalarArrayDiffAlgorithm = ( currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -72,6 +74,7 @@ interface MergeArgs { currentVersion: TValue[]; targetVersion: TValue[]; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ @@ -79,15 +82,13 @@ const mergeVersions = ({ currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { const dedupedBaseVersion = uniq(baseVersion); const dedupedCurrentVersion = uniq(currentVersion); const dedupedTargetVersion = uniq(targetVersion); switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: @@ -118,16 +119,34 @@ const mergeVersions = ({ mergeOutcome: ThreeWayMergeOutcome.Merged, }; } - // Scenario -AB is treated as scenario ABC, but marked as - // SOLVABLE, and returns the target version as the merged version - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseCanUpdate: { + + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { return { + conflict: ThreeWayDiffConflict.NONE, mergedVersion: targetVersion, mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, }; } + + // If the rule is customized, we return a SOLVABLE conflict with a merged outcome + // Otherwise we treat scenario -AB as AAB + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseCanUpdate: { + return isRuleCustomized + ? { + mergedVersion: union(dedupedCurrentVersion, dedupedTargetVersion), + mergeOutcome: ThreeWayMergeOutcome.Merged, + conflict: ThreeWayDiffConflict.SOLVABLE, + } + : { + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }; + } default: return assertUnreachable(diffOutcome); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/simple_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/simple_diff_algorithm.ts index 1673b87003a00..f0b3ef9c66d9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/simple_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/simple_diff_algorithm.ts @@ -25,7 +25,8 @@ import { * Meant to be used with primitive types (strings, numbers, booleans), NOT Arrays or Objects */ export const simpleDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -39,10 +40,10 @@ export const simpleDiffAlgorithm = ( const hasBaseVersion = baseVersion !== MissingVersion; const { mergeOutcome, conflict, mergedVersion } = mergeVersions({ - hasBaseVersion, currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -66,22 +67,19 @@ interface MergeResult { } interface MergeArgs { - hasBaseVersion: boolean; currentVersion: TValue; targetVersion: TValue; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ - hasBaseVersion, currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: @@ -106,14 +104,26 @@ const mergeVersions = ({ }; } - // Scenario -AB is treated as scenario ABC, but marked as - // SOLVABLE, and returns the target version as the merged version - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { + return { + conflict: ThreeWayDiffConflict.NONE, + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + }; + } + + // Missing base versions always return target version + // If the rule is customized, we return a SOLVABLE conflict + // Otherwise we treat scenario -AB as AAB + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { + conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE, mergedVersion: targetVersion, mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, }; } default: diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts index f80d8b63c8da8..f019e6298c850 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts @@ -9,5 +9,6 @@ import type { ThreeVersionsOf } from '../../../../../../../../common/api/detecti import { simpleDiffAlgorithm } from './simple_diff_algorithm'; export const singleLineStringDiffAlgorithm = ( - versions: ThreeVersionsOf -) => simpleDiffAlgorithm(versions); + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts index b861a8432797b..77895e1b84484 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts @@ -60,9 +60,10 @@ const TARGET_TYPE_ERROR = `Target version can't be of different rule type`; * three-way diffs calculated for those fields. */ export const calculateRuleFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean = false ): RuleFieldsDiff => { - const commonFieldsDiff = calculateCommonFieldsDiff(ruleVersions); + const commonFieldsDiff = calculateCommonFieldsDiff(ruleVersions, isRuleCustomized); // eslint-disable-next-line @typescript-eslint/naming-convention const { base_version, current_version, target_version } = ruleVersions; const hasBaseVersion = base_version !== MissingVersion; @@ -78,11 +79,14 @@ export const calculateRuleFieldsDiff = ( // only for fields of a single rule type, and need to calculate it for all fields // of all the rule types we have. // TODO: Try to get rid of "as" casting - return calculateAllFieldsDiff({ - base_version: base_version as DiffableAllFields | MissingVersion, - current_version: current_version as DiffableAllFields, - target_version: target_version as DiffableAllFields, - }) as RuleFieldsDiff; + return calculateAllFieldsDiff( + { + base_version: base_version as DiffableAllFields | MissingVersion, + current_version: current_version as DiffableAllFields, + target_version: target_version as DiffableAllFields, + }, + isRuleCustomized + ) as RuleFieldsDiff; } switch (current_version.type) { @@ -93,7 +97,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'query', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateCustomQueryFieldsDiff({ base_version, current_version, target_version }), + ...calculateCustomQueryFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'saved_query': { @@ -103,7 +110,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'saved_query', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateSavedQueryFieldsDiff({ base_version, current_version, target_version }), + ...calculateSavedQueryFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'eql': { @@ -113,7 +123,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'eql', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateEqlFieldsDiff({ base_version, current_version, target_version }), + ...calculateEqlFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'threat_match': { @@ -123,7 +136,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'threat_match', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateThreatMatchFieldsDiff({ base_version, current_version, target_version }), + ...calculateThreatMatchFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'threshold': { @@ -133,7 +149,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'threshold', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateThresholdFieldsDiff({ base_version, current_version, target_version }), + ...calculateThresholdFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'machine_learning': { @@ -143,7 +162,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'machine_learning', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateMachineLearningFieldsDiff({ base_version, current_version, target_version }), + ...calculateMachineLearningFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'new_terms': { @@ -153,7 +175,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'new_terms', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateNewTermsFieldsDiff({ base_version, current_version, target_version }), + ...calculateNewTermsFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'esql': { @@ -163,7 +188,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'esql', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateEsqlFieldsDiff({ base_version, current_version, target_version }), + ...calculateEsqlFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } default: { @@ -173,9 +201,10 @@ export const calculateRuleFieldsDiff = ( }; const calculateCommonFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): CommonFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, commonFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, commonFieldsDiffAlgorithms, isRuleCustomized); }; const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -209,9 +238,10 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor }; const calculateCustomQueryFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): CustomQueryFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, customQueryFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, customQueryFieldsDiffAlgorithms, isRuleCustomized); }; const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -222,9 +252,10 @@ const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): SavedQueryFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, savedQueryFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, savedQueryFieldsDiffAlgorithms, isRuleCustomized); }; const savedQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -235,9 +266,10 @@ const savedQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): EqlFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, eqlFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, eqlFieldsDiffAlgorithms, isRuleCustomized); }; const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -248,9 +280,10 @@ const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { }; const calculateEsqlFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): EsqlFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, esqlFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, esqlFieldsDiffAlgorithms, isRuleCustomized); }; const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -260,9 +293,10 @@ const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { }; const calculateThreatMatchFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreatMatchFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, threatMatchFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, threatMatchFieldsDiffAlgorithms, isRuleCustomized); }; const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -277,9 +311,10 @@ const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThresholdFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, thresholdFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, thresholdFieldsDiffAlgorithms, isRuleCustomized); }; const thresholdFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -291,9 +326,14 @@ const thresholdFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): MachineLearningFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, machineLearningFieldsDiffAlgorithms); + return calculateFieldsDiffFor( + ruleVersions, + machineLearningFieldsDiffAlgorithms, + isRuleCustomized + ); }; const machineLearningFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = @@ -305,9 +345,10 @@ const machineLearningFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): NewTermsFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, newTermsFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, newTermsFieldsDiffAlgorithms, isRuleCustomized); }; const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -320,9 +361,10 @@ const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): AllFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, allFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, allFieldsDiffAlgorithms, isRuleCustomized); }; const allFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/diff_calculation_helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/diff_calculation_helpers.ts index eba89041260ce..9fe0a958eab2b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/diff_calculation_helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/diff_calculation_helpers.ts @@ -15,11 +15,12 @@ import { MissingVersion } from '../../../../../../../common/api/detection_engine export const calculateFieldsDiffFor = ( ruleVersions: ThreeVersionsOf, - fieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + fieldsDiffAlgorithms: FieldsDiffAlgorithmsFor, + isRuleCustomized: boolean ): FieldsDiff => { const result = mapValues(fieldsDiffAlgorithms, (calculateFieldDiff, fieldName) => { const fieldVersions = pickField(fieldName as keyof TObject, ruleVersions); - const fieldDiff = calculateFieldDiff(fieldVersions); + const fieldDiff = calculateFieldDiff(fieldVersions, isRuleCustomized); return fieldDiff; }); From 7e1de38ed06d6dce7b65c97d4d895b79803612cf Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Thu, 13 Feb 2025 17:50:39 +0100 Subject: [PATCH 02/18] Reduce the _review rule upgrade endpoint response size --- .../get_prebuilt_rules_status_route.ts | 12 ++ .../perform_rule_upgrade_route.ts | 13 +- ... => use_perform_rules_upgrade_mutation.ts} | 30 +-- .../use_perform_rule_upgrade.ts | 12 +- .../use_prebuilt_rules_upgrade_review.ts | 8 +- .../rule_management/logic/types.ts | 12 +- .../rule_update_callouts.tsx | 4 +- .../add_prebuilt_rules_table_context.tsx | 2 +- .../rules_table/rules_table_toolbar.tsx | 2 +- .../translations.tsx | 7 - .../upgrade_prebuilt_rules_table.tsx | 43 +++- .../upgrade_prebuilt_rules_table_buttons.tsx | 28 +-- .../upgrade_prebuilt_rules_table_context.tsx | 202 ++++++++++++++---- .../upgrade_prebuilt_rules_table_filters.tsx | 36 ++-- ...rade_rule_customization_filter_popover.tsx | 35 ++- .../use_filter_prebuilt_rules_to_upgrade.ts | 48 ----- .../add_elastic_rules_button.tsx | 2 +- .../get_prebuilt_rules_status_route.ts | 5 + .../get_upgradeable_rules.ts | 23 +- .../perform_rule_upgrade_route.ts | 31 ++- .../calculate_rule_upgrade_info.ts | 59 +++++ .../review_rule_upgrade_handler.ts | 177 ++++++++------- .../prebuilt_rule_objects_client.ts | 41 +++- .../fetch_rule_versions_triad.ts | 10 +- .../search/get_existing_prepackaged_rules.ts | 14 +- 25 files changed, 556 insertions(+), 300 deletions(-) rename x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/{use_perform_specific_rules_upgrade_mutation.ts => use_perform_rules_upgrade_mutation.ts} (79%) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index e76ba63cfa17d..1a9e70f84cd2c 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -8,6 +8,18 @@ export interface GetPrebuiltRulesStatusResponseBody { /** Aggregated info about all prebuilt rules */ stats: PrebuiltRulesStatusStats; + + /** + * Aggregated info about upgradeable prebuilt rules. This fields is optional + * for backward compatibility. After one serverless release cycle, it can be + * made required. + * */ + aggregated_fields?: { + upgradeable_rules: { + /** List of all tags of the current versions of upgradeable rules */ + tags: string[]; + }; + }; } export interface PrebuiltRulesStatusStats { diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts index ac75bbb56e0bf..06f105a3ef281 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -10,6 +10,7 @@ import { mapValues } from 'lodash'; import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen'; import { AggregatedPrebuiltRuleError, DiffableAllFields } from '../model'; import { RuleSignatureId, RuleVersion } from '../../model'; +import { PrebuiltRuleFilter } from '../common/prebuilt_rule_filter'; export type Mode = z.infer; export const Mode = z.enum(['ALL_RULES', 'SPECIFIC_RULES']); @@ -111,21 +112,31 @@ export const RuleUpgradeSpecifier = z.object({ fields: RuleFieldsToUpgrade.optional(), }); +export type UpgradeConflictResolution = z.infer; +export const UpgradeConflictResolution = z.enum(['SKIP', 'OVERWRITE']); +export type UpgradeConflictResolutionEnum = typeof UpgradeConflictResolution.enum; +export const UpgradeConflictResolutionEnum = UpgradeConflictResolution.enum; + export type UpgradeSpecificRulesRequest = z.infer; export const UpgradeSpecificRulesRequest = z.object({ mode: z.literal('SPECIFIC_RULES'), rules: z.array(RuleUpgradeSpecifier).min(1), pick_version: PickVersionValues.optional(), + on_conflict: UpgradeConflictResolution.optional(), + dry_run: z.boolean().optional(), }); export type UpgradeAllRulesRequest = z.infer; export const UpgradeAllRulesRequest = z.object({ mode: z.literal('ALL_RULES'), pick_version: PickVersionValues.optional(), + filter: PrebuiltRuleFilter.optional(), + on_conflict: UpgradeConflictResolution.optional(), + dry_run: z.boolean().optional(), }); export type SkipRuleUpgradeReason = z.infer; -export const SkipRuleUpgradeReason = z.enum(['RULE_UP_TO_DATE']); +export const SkipRuleUpgradeReason = z.enum(['RULE_UP_TO_DATE', 'CONFLICT']); export type SkipRuleUpgradeReasonEnum = typeof SkipRuleUpgradeReason.enum; export const SkipRuleUpgradeReasonEnum = SkipRuleUpgradeReason.enum; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation.ts similarity index 79% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation.ts index 84b074449603d..ecb604d63d4e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation.ts @@ -6,10 +6,12 @@ */ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; -import type { PerformRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { + PerformRuleUpgradeRequestBody, + PerformRuleUpgradeResponseBody, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; -import type { PerformUpgradeRequest } from '../../api'; -import { performUpgradeSpecificRules } from '../../api'; +import { performUpgradeRules } from '../../api'; import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; @@ -19,14 +21,14 @@ import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './use_fetch_p import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; import { cappedExponentialBackoff } from './capped_exponential_backoff'; -export const PERFORM_SPECIFIC_RULES_UPGRADE_KEY = [ - 'POST', - 'SPECIFIC_RULES', - PERFORM_RULE_UPGRADE_URL, -]; +export const PERFORM_RULES_UPGRADE_KEY = ['POST', PERFORM_RULE_UPGRADE_URL]; -export const usePerformSpecificRulesUpgradeMutation = ( - options?: UseMutationOptions +export const usePerformRulesUpgradeMutation = ( + options?: UseMutationOptions< + PerformRuleUpgradeResponseBody, + unknown, + PerformRuleUpgradeRequestBody + > ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); @@ -37,13 +39,13 @@ export const usePerformSpecificRulesUpgradeMutation = ( const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); const invalidateFetchCoverageOverviewQuery = useInvalidateFetchCoverageOverviewQuery(); - return useMutation( - (args: PerformUpgradeRequest) => { - return performUpgradeSpecificRules(args); + return useMutation( + (args: PerformRuleUpgradeRequestBody) => { + return performUpgradeRules(args); }, { ...options, - mutationKey: PERFORM_SPECIFIC_RULES_UPGRADE_KEY, + mutationKey: PERFORM_RULES_UPGRADE_KEY, onSettled: (...args) => { invalidatePrePackagedRulesStatus(); invalidateFindRulesQuery(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts index 33f36ffe14da2..e1ae2768dacc6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts @@ -5,18 +5,22 @@ * 2.0. */ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { usePerformSpecificRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation'; +import { usePerformRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation'; import * as i18n from './translations'; -export const usePerformUpgradeSpecificRules = () => { +export const usePerformUpgradeRules = () => { const { addError, addSuccess } = useAppToasts(); - return usePerformSpecificRulesUpgradeMutation({ + return usePerformRulesUpgradeMutation({ onError: (err) => { addError(err, { title: i18n.RULE_UPGRADE_FAILED }); }, - onSuccess: (result) => { + onSuccess: (result, vars) => { + if (vars.dry_run) { + // This is a preflight check, no need to show toast + return; + } addSuccess(getSuccessToastMessage(result)); }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts index 6e8f008c5ede5..bb0b04174d0dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts @@ -5,7 +5,10 @@ * 2.0. */ import type { UseQueryOptions } from '@tanstack/react-query'; -import type { ReviewRuleUpgradeResponseBody } from '../../../../../common/api/detection_engine/prebuilt_rules'; +import type { + ReviewRuleUpgradeRequestBody, + ReviewRuleUpgradeResponseBody, +} from '../../../../../common/api/detection_engine/prebuilt_rules'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import * as i18n from '../translations'; @@ -18,11 +21,12 @@ import { useFetchPrebuiltRulesUpgradeReviewQuery } from '../../api/hooks/prebuil * @returns useQuery result */ export const usePrebuiltRulesUpgradeReview = ( + request: ReviewRuleUpgradeRequestBody, options?: UseQueryOptions ) => { const { addError } = useAppToasts(); - return useFetchPrebuiltRulesUpgradeReviewQuery({ + return useFetchPrebuiltRulesUpgradeReviewQuery(request, { onError: (error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }), ...options, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index e91639dd0454c..98383e93b444c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -11,7 +11,10 @@ import type { RuleSnooze } from '@kbn/alerting-plugin/common'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types'; -import type { WarningSchema } from '../../../../common/api/detection_engine'; +import type { + RuleCustomizationStatus, + WarningSchema, +} from '../../../../common/api/detection_engine'; import type { RuleExecutionStatus } from '../../../../common/api/detection_engine/rule_monitoring'; import { SortOrder } from '../../../../common/api/detection_engine'; @@ -103,7 +106,7 @@ export interface FilterOptions { excludeRuleTypes?: Type[]; enabled?: boolean; // undefined is to display all the rules ruleExecutionStatus?: RuleExecutionStatus; // undefined means "all" - ruleSource?: RuleCustomizationEnum[]; // undefined is to display all the rules + ruleSource?: RuleCustomizationStatus[]; // undefined is to display all the rules showRulesWithGaps?: boolean; gapSearchRange?: GapRangeValue; } @@ -209,8 +212,3 @@ export interface FindRulesReferencedByExceptionsProps { lists: FindRulesReferencedByExceptionsListProp[]; signal?: AbortSignal; } - -export enum RuleCustomizationEnum { - customized = 'CUSTOMIZED', - not_customized = 'NOT_CUSTOMIZED', -} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx index c4eacb4a01ff5..6ca59501dbd7a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx @@ -21,8 +21,8 @@ import { AllRulesTabs } from '../rules_table/rules_table_toolbar'; export const RuleUpdateCallouts = () => { const { data: prebuiltRulesStatus } = usePrebuiltRulesStatus(); - const rulesToInstallCount = prebuiltRulesStatus?.num_prebuilt_rules_to_install ?? 0; - const rulesToUpgradeCount = prebuiltRulesStatus?.num_prebuilt_rules_to_upgrade ?? 0; + const rulesToInstallCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_install ?? 0; + const rulesToUpgradeCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_upgrade ?? 0; // Check against rulesInstalledCount since we don't want to show banners if we're showing the empty prompt const shouldDisplayNewRulesCallout = rulesToInstallCount > 0; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index bb949ba436995..59d66a402dc88 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -144,7 +144,7 @@ export const AddPrebuiltRulesTableContextProvider = ({ enabled: isUpgradeReviewRequestEnabled({ canUserCRUD, isUpgradingSecurityPackages, - prebuiltRulesStatus, + prebuiltRulesStatus: prebuiltRulesStatus?.stats, }), }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx index e3618a1383598..eb9d17ebfbe9c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx @@ -33,7 +33,7 @@ export const RulesTableToolbar = React.memo(() => { const installedTotal = (ruleManagementFilters?.rules_summary.custom_count ?? 0) + (ruleManagementFilters?.rules_summary.prebuilt_installed_count ?? 0); - const updateTotal = prebuiltRulesStatus?.num_prebuilt_rules_to_upgrade ?? 0; + const updateTotal = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_upgrade ?? 0; const shouldDisplayRuleUpdatesTab = !loading && canUserCRUD && updateTotal > 0; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx index b3564dd2fd704..30055cef4c2ec 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx @@ -34,13 +34,6 @@ export const BULK_UPDATE_BUTTON_TOOLTIP_NO_PERMISSIONS = i18n.translate( } ); -export const BULK_UPDATE_ALL_RULES_BUTTON_TOOLTIP_CONFLICTS = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.bulkButtons.allRules.conflicts', - { - defaultMessage: 'All rules have conflicts. Update them individually.', - } -); - export const BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.upgradeRules.bulkButtons.selectedRules.conflicts', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx index dfe8c5787417c..959f20ef72c20 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, + EuiBasicTable, EuiProgress, EuiSkeletonLoading, EuiSkeletonText, @@ -19,9 +19,10 @@ import { import React, { useCallback, useState } from 'react'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; -import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants'; +import { RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants'; import { RulesChangelogLink } from '../rules_changelog_link'; import { UpgradePrebuiltRulesTableButtons } from './upgrade_prebuilt_rules_table_buttons'; +import type { UpgradePrebuiltRulesSortingOptions } from './upgrade_prebuilt_rules_table_context'; import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; import { UpgradePrebuiltRulesTableFilters } from './upgrade_prebuilt_rules_table_filters'; import { useUpgradePrebuiltRulesTableColumns } from './use_upgrade_prebuilt_rules_table_columns'; @@ -44,20 +45,32 @@ export const UpgradePrebuiltRulesTable = React.memo(() => { ruleUpgradeStates, hasRulesToUpgrade, isLoading, + isFetching, isRefetching, isUpgradingSecurityPackages, + pagination, + sortingOptions, }, + actions: { setPagination, setSortingOptions }, } = useUpgradePrebuiltRulesTableContext(); const [selected, setSelected] = useState([]); const rulesColumns = useUpgradePrebuiltRulesTableColumns(); const shouldShowProgress = isUpgradingSecurityPackages || isRefetching; - const [pageIndex, setPageIndex] = useState(0); const handleTableChange = useCallback( - ({ page: { index } }: CriteriaWithPagination) => { - setPageIndex(index); + ({ page: { index, size }, sort }: CriteriaWithPagination) => { + setPagination({ + page: index + 1, + perPage: size, + }); + if (sort) { + setSortingOptions({ + field: sort.field as UpgradePrebuiltRulesSortingOptions['field'], + order: sort.direction, + }); + } }, - [setPageIndex] + [setPagination, setSortingOptions] ); return ( @@ -104,23 +117,31 @@ export const UpgradePrebuiltRulesTable = React.memo(() => { - true, onSelectionChange: setSelected, initialSelected: selected, }} + sorting={{ + sort: { + // EuiBasicTable has incorrect `sort.field` types which accept only `keyof Item` and reject fields in dot notation + field: sortingOptions.field as keyof RuleUpgradeState, + direction: sortingOptions.order, + }, + }} itemId="rule_id" data-test-subj="rules-upgrades-table" columns={rulesColumns} - onTableChange={handleTableChange} + onChange={handleTableChange} /> ) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx index 37e189f5e4b79..5498e8961c06a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx @@ -21,13 +21,7 @@ export const UpgradePrebuiltRulesTableButtons = ({ selectedRules, }: UpgradePrebuiltRulesTableButtonsProps) => { const { - state: { - ruleUpgradeStates, - hasRulesToUpgrade, - loadingRules, - isRefetching, - isUpgradingSecurityPackages, - }, + state: { hasRulesToUpgrade, loadingRules, isRefetching, isUpgradingSecurityPackages }, actions: { upgradeRules, upgradeAllRules }, } = useUpgradePrebuiltRulesTableContext(); const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus(); @@ -43,14 +37,10 @@ export const UpgradePrebuiltRulesTableButtons = ({ const doAllSelectedRulesHaveConflicts = isRulesCustomizationEnabled && selectedRules.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts); - const doAllRulesHaveConflicts = - isRulesCustomizationEnabled && - ruleUpgradeStates.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts); const { selectedRulesButtonTooltip, allRulesButtonTooltip } = useBulkUpdateButtonsTooltipContent({ canUserEditRules, doAllSelectedRulesHaveConflicts, - doAllRulesHaveConflicts, isPrebuiltRulesCustomizationEnabled: isRulesCustomizationEnabled, }); @@ -83,12 +73,7 @@ export const UpgradePrebuiltRulesTableButtons = ({ fill iconType="plusInCircle" onClick={upgradeAllRules} - disabled={ - !canUserEditRules || - !hasRulesToUpgrade || - isRequestInProgress || - doAllRulesHaveConflicts - } + disabled={!canUserEditRules || !hasRulesToUpgrade || isRequestInProgress} data-test-subj="upgradeAllRulesButton" > {i18n.UPDATE_ALL} @@ -103,12 +88,10 @@ export const UpgradePrebuiltRulesTableButtons = ({ const useBulkUpdateButtonsTooltipContent = ({ canUserEditRules, doAllSelectedRulesHaveConflicts, - doAllRulesHaveConflicts, isPrebuiltRulesCustomizationEnabled, }: { canUserEditRules: boolean | null; doAllSelectedRulesHaveConflicts: boolean; - doAllRulesHaveConflicts: boolean; isPrebuiltRulesCustomizationEnabled: boolean; }) => { if (!canUserEditRules) { @@ -125,13 +108,6 @@ const useBulkUpdateButtonsTooltipContent = ({ }; } - if (doAllRulesHaveConflicts) { - return { - selectedRulesButtonTooltip: i18n.BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS, - allRulesButtonTooltip: i18n.BULK_UPDATE_ALL_RULES_BUTTON_TOOLTIP_CONFLICTS, - }; - } - if (doAllSelectedRulesHaveConflicts) { return { selectedRulesButtonTooltip: i18n.BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 3be956c7cbf94..7253c9fb8af02 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -10,8 +10,11 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; import type { + FindRulesSortField, + PrebuiltRuleFilter, RuleFieldsToUpgrade, RuleUpgradeSpecifier, + SortOrder, } from '../../../../../../common/api/detection_engine'; import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; @@ -24,13 +27,11 @@ import type { } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { invariant } from '../../../../../../common/utils/invariant'; import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout'; -import { usePerformUpgradeSpecificRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; +import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; import { FieldUpgradeStateEnum } from '../../../../rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum'; import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; -import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_upgrade'; -import { useFilterPrebuiltRulesToUpgrade } from './use_filter_prebuilt_rules_to_upgrade'; import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; import { useOutdatedMlJobsUpgradeModal } from './use_ml_jobs_upgrade_modal'; import { useUpgradeWithConflictsModal } from './use_upgrade_with_conflicts_modal'; @@ -39,9 +40,21 @@ import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader'; import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; import * as i18n from './translations'; import { CustomizationDisabledCallout } from './customization_disabled_callout'; +import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../constants'; +import type { PaginationOptions } from '../../../../rule_management/logic'; +import { usePrebuiltRulesStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; +export interface UpgradePrebuiltRulesSortingOptions { + field: + | 'current_rule.name' + | 'current_rule.risk_score' + | 'current_rule.severity' + | 'current_rule.last_updated'; + order: SortOrder; +} + export interface UpgradePrebuiltRulesTableState { /** * Rule upgrade state after applying `filterOptions` @@ -50,7 +63,7 @@ export interface UpgradePrebuiltRulesTableState { /** * Currently selected table filter */ - filterOptions: UpgradePrebuiltRulesTableFilterOptions; + filterOptions: PrebuiltRuleFilter; /** * All unique tags for all rules */ @@ -63,6 +76,10 @@ export interface UpgradePrebuiltRulesTableState { * Is true then there is no cached data and the query is currently fetching. */ isLoading: boolean; + /** + * Is true whenever a request is in-flight, which includes initial loading as well as background refetches. + */ + isFetching: boolean; /** * Will be true if the query has been fetched. */ @@ -84,6 +101,14 @@ export interface UpgradePrebuiltRulesTableState { * The timestamp for when the rules were successfully fetched */ lastUpdated: number; + /** + * Current pagination state + */ + pagination: PaginationOptions; + /** + * Currently selected table sorting + */ + sortingOptions: UpgradePrebuiltRulesSortingOptions; } export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; @@ -92,7 +117,9 @@ export interface UpgradePrebuiltRulesTableActions { reFetchRules: () => void; upgradeRules: (ruleIds: RuleSignatureId[]) => void; upgradeAllRules: () => void; - setFilterOptions: Dispatch>; + setFilterOptions: Dispatch>; + setPagination: Dispatch>; + setSortingOptions: Dispatch>; openRulePreview: (ruleId: string) => void; } @@ -121,35 +148,71 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ }: UpgradePrebuiltRulesTableContextProviderProps) => { const { isRulesCustomizationEnabled, customizationDisabledReason } = usePrebuiltRulesCustomizationStatus(); + + // Use the data from the prebuilt rules status API to determine if there are + // rules to upgrade because it returns information about all rules without filters + const { data: prebuiltRulesStatusResponse } = usePrebuiltRulesStatus(); + const hasRulesToUpgrade = + (prebuiltRulesStatusResponse?.stats.num_prebuilt_rules_to_upgrade ?? 0) > 0; + const tags = prebuiltRulesStatusResponse?.aggregated_fields?.upgradeable_rules.tags; + const [loadingRules, setLoadingRules] = useState([]); - const [filterOptions, setFilterOptions] = useState({ - filter: '', - tags: [], - ruleSource: [], - }); + const [filterOptions, setFilterOptions] = useState({}); const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); + const [pagination, setPagination] = useState({ + page: 1, + perPage: RULES_TABLE_INITIAL_PAGE_SIZE, + }); + const [sortingOptions, setSortingOptions] = useState({ + field: 'current_rule.last_updated', + order: 'asc', + }); + + const findRulesSortField = useMemo( + () => + (( + { + 'current_rule.name': 'name', + 'current_rule.risk_score': 'risk_score', + 'current_rule.severity': 'severity', + 'current_rule.last_updated': 'updated_at', + } as const + )[sortingOptions.field]), + [sortingOptions.field] + ); const { - data: { rules: ruleUpgradeInfos, stats: { tags } } = { - rules: [], - stats: { tags: [] }, - }, + data: upgradeReviewResponse, refetch, dataUpdatedAt, isFetched, isLoading, + isFetching, isRefetching, - } = usePrebuiltRulesUpgradeReview({ - refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL, - keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change - }); + } = usePrebuiltRulesUpgradeReview( + { + page: pagination.page, + per_page: pagination.perPage, + sort: { + field: findRulesSortField, + order: sortingOptions.order, + }, + filter: filterOptions, + }, + { + refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL, + keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change + } + ); + + const upgradeableRules = useMemo( + () => upgradeReviewResponse?.rules ?? [], + [upgradeReviewResponse] + ); + const { rulesUpgradeState, setRuleFieldResolvedValue } = - usePrebuiltRulesUpgradeState(ruleUpgradeInfos); + usePrebuiltRulesUpgradeState(upgradeableRules); const ruleUpgradeStates = useMemo(() => Object.values(rulesUpgradeState), [rulesUpgradeState]); - const filteredRuleUpgradeStates = useFilterPrebuiltRulesToUpgrade({ - filterOptions, - data: ruleUpgradeStates, - }); const { modal: confirmLegacyMlJobsUpgradeModal, @@ -158,7 +221,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ } = useOutdatedMlJobsUpgradeModal(); const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal(); - const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules(); + const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules(); const upgradeRulesToResolved = useCallback( async (ruleIds: RuleSignatureId[]) => { @@ -189,8 +252,9 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ return; } - await upgradeSpecificRulesRequest({ - pickVersion: 'MERGED', + await upgradeRulesRequest({ + mode: 'SPECIFIC_RULES', + pick_version: 'MERGED', rules: ruleUpgradeSpecifiers, }); } catch { @@ -201,7 +265,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); } }, - [confirmLegacyMLJobs, confirmConflictsUpgrade, rulesUpgradeState, upgradeSpecificRulesRequest] + [confirmLegacyMLJobs, confirmConflictsUpgrade, rulesUpgradeState, upgradeRulesRequest] ); const upgradeRulesToTarget = useCallback( @@ -220,8 +284,9 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ return; } - await upgradeSpecificRulesRequest({ - pickVersion: 'TARGET', + await upgradeRulesRequest({ + mode: 'SPECIFIC_RULES', + pick_version: 'TARGET', rules: ruleUpgradeSpecifiers, }); } catch { @@ -232,7 +297,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); } }, - [confirmLegacyMLJobs, rulesUpgradeState, upgradeSpecificRulesRequest] + [confirmLegacyMLJobs, rulesUpgradeState, upgradeRulesRequest] ); const upgradeRules = useCallback( @@ -246,11 +311,50 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ [isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget] ); - const upgradeAllRules = useCallback( - // Upgrade all rules, ignoring filter and selection - () => upgradeRules(ruleUpgradeInfos.map((rule) => rule.rule_id)), - [ruleUpgradeInfos, upgradeRules] - ); + const upgradeAllRules = useCallback(async () => { + setLoadingRules((prev) => [...prev, ...upgradeableRules.map((rule) => rule.rule_id)]); + + try { + // Handle MLJobs modal + if (!(await confirmLegacyMLJobs())) { + return; + } + + const dryRunResults = await upgradeRulesRequest({ + mode: 'ALL_RULES', + pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', + filter: filterOptions, + dry_run: true, + on_conflict: 'SKIP', + }); + + const hasConflicts = dryRunResults.results.skipped.some( + (skippedRule) => skippedRule.reason === 'CONFLICT' + ); + + if (hasConflicts && !(await confirmConflictsUpgrade())) { + return; + } + + await upgradeRulesRequest({ + mode: 'ALL_RULES', + pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', + filter: filterOptions, + on_conflict: 'SKIP', + }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here + } finally { + setLoadingRules([]); + } + }, [ + upgradeableRules, + confirmLegacyMLJobs, + upgradeRulesRequest, + isRulesCustomizationEnabled, + filterOptions, + confirmConflictsUpgrade, + ]); const subHeaderFactory = useCallback( (rule: RuleResponse) => @@ -377,12 +481,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setRuleFieldResolvedValue, ] ); - const filteredRules = useMemo( - () => filteredRuleUpgradeStates.map(({ target_rule: targetRule }) => targetRule), - [filteredRuleUpgradeStates] - ); const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ - rules: filteredRules, + rules: ruleUpgradeStates.map(({ target_rule: targetRule }) => targetRule), subHeaderFactory, ruleActionsFactory, extraTabsFactory, @@ -399,6 +499,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ upgradeAllRules, setFilterOptions, openRulePreview, + setPagination, + setSortingOptions, }), [refetch, upgradeRules, upgradeAllRules, openRulePreview] ); @@ -406,31 +508,41 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ const providerValue = useMemo( () => ({ state: { - ruleUpgradeStates: filteredRuleUpgradeStates, - hasRulesToUpgrade: isFetched && ruleUpgradeInfos.length > 0, + ruleUpgradeStates, + hasRulesToUpgrade, filterOptions, - tags, + tags: tags ?? [], isFetched, isLoading: isLoading || areMlJobsLoading, + isFetching, isRefetching, isUpgradingSecurityPackages, loadingRules, lastUpdated: dataUpdatedAt, + pagination: { + ...pagination, + total: upgradeReviewResponse?.total ?? 0, + }, + sortingOptions, }, actions, }), [ - ruleUpgradeInfos.length, - filteredRuleUpgradeStates, + ruleUpgradeStates, + hasRulesToUpgrade, filterOptions, tags, isFetched, isLoading, areMlJobsLoading, + isFetching, isRefetching, isUpgradingSecurityPackages, loadingRules, dataUpdatedAt, + pagination, + upgradeReviewResponse?.total, + sortingOptions, actions, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx index d8b8f618cf43d..b62f987b8abe2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx @@ -9,11 +9,11 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; import React, { useCallback } from 'react'; import styled from 'styled-components'; +import type { RuleCustomizationStatus } from '../../../../../../common/api/detection_engine'; import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status'; -import type { RuleCustomizationEnum } from '../../../../rule_management/logic'; -import * as i18n from './translations'; -import { TagsFilterPopover } from '../rules_table_filters/tags_filter_popover'; import { RuleSearchField } from '../rules_table_filters/rule_search_field'; +import { TagsFilterPopover } from '../rules_table_filters/tags_filter_popover'; +import * as i18n from './translations'; import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; import { RuleCustomizationFilterPopover } from './upgrade_rule_customization_filter_popover'; @@ -33,13 +33,13 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus(); - const { tags: selectedTags, ruleSource: selectedRuleSource = [] } = filterOptions; + const { tags: selectedTags, customization_status: customizationStatus } = filterOptions; const handleOnSearch = useCallback( - (filterString: string) => { + (nameString: string) => { setFilterOptions((filters) => ({ ...filters, - filter: filterString.trim(), + name: nameString.trim(), })); }, [setFilterOptions] @@ -57,22 +57,20 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { [selectedTags, setFilterOptions] ); - const handleSelectedRuleSource = useCallback( - (newRuleSource: RuleCustomizationEnum[]) => { - if (!isEqual(newRuleSource, selectedRuleSource)) { - setFilterOptions((filters) => ({ - ...filters, - ruleSource: newRuleSource, - })); - } + const handleCustomizationStatusChange = useCallback( + (newCustomizationStatus: RuleCustomizationStatus | undefined) => { + setFilterOptions((filters) => ({ + ...filters, + customization_status: newCustomizationStatus, + })); }, - [selectedRuleSource, setFilterOptions] + [setFilterOptions] ); return ( @@ -81,8 +79,8 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { {isRulesCustomizationEnabled && ( @@ -90,7 +88,7 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx index 234943e333272..05e68f4ebbccb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx @@ -5,23 +5,22 @@ * 2.0. */ -import React, { useState, useMemo } from 'react'; import type { EuiSelectableOption } from '@elastic/eui'; import { EuiFilterButton, EuiPopover, EuiSelectable } from '@elastic/eui'; -import { RuleCustomizationEnum } from '../../../../rule_management/logic'; +import React, { useMemo, useState } from 'react'; +import { RuleCustomizationStatus } from '../../../../../../common/api/detection_engine'; import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; -import { toggleSelectedGroup } from '../../../../../common/components/ml_popover/jobs_table/filters/toggle_selected_group'; interface RuleCustomizationFilterPopoverProps { - selectedRuleSource: RuleCustomizationEnum[]; - onSelectedRuleSourceChanged: (newRuleSource: RuleCustomizationEnum[]) => void; + customizationStatus: RuleCustomizationStatus | undefined; + onCustomizationStatusChanged: (newRuleSource: RuleCustomizationStatus | undefined) => void; } const RULE_CUSTOMIZATION_POPOVER_WIDTH = 200; const RuleCustomizationFilterPopoverComponent = ({ - selectedRuleSource, - onSelectedRuleSourceChanged, + customizationStatus, + onCustomizationStatusChanged, }: RuleCustomizationFilterPopoverProps) => { const [isRuleCustomizationPopoverOpen, setIsRuleCustomizationPopoverOpen] = useState(false); @@ -29,18 +28,16 @@ const RuleCustomizationFilterPopoverComponent = ({ () => [ { label: i18n.MODIFIED_LABEL, - key: RuleCustomizationEnum.customized, - checked: selectedRuleSource.includes(RuleCustomizationEnum.customized) ? 'on' : undefined, + key: RuleCustomizationStatus.CUSTOMIZED, + checked: customizationStatus === RuleCustomizationStatus.CUSTOMIZED ? 'on' : undefined, }, { label: i18n.UNMODIFIED_LABEL, - key: RuleCustomizationEnum.not_customized, - checked: selectedRuleSource.includes(RuleCustomizationEnum.not_customized) - ? 'on' - : undefined, + key: RuleCustomizationStatus.NOT_CUSTOMIZED, + checked: customizationStatus === RuleCustomizationStatus.NOT_CUSTOMIZED ? 'on' : undefined, }, ], - [selectedRuleSource] + [customizationStatus] ); const handleSelectableOptionsChange = ( @@ -48,10 +45,8 @@ const RuleCustomizationFilterPopoverComponent = ({ _: unknown, changedOption: EuiSelectableOption ) => { - toggleSelectedGroup( - changedOption.key ?? '', - selectedRuleSource, - onSelectedRuleSourceChanged as (args: string[]) => void + onCustomizationStatusChanged( + changedOption.checked === 'on' ? (changedOption.key as RuleCustomizationStatus) : undefined ); }; @@ -62,8 +57,8 @@ const RuleCustomizationFilterPopoverComponent = ({ onClick={() => setIsRuleCustomizationPopoverOpen(!isRuleCustomizationPopoverOpen)} numFilters={selectableOptions.length} isSelected={isRuleCustomizationPopoverOpen} - hasActiveFilters={selectedRuleSource.length > 0} - numActiveFilters={selectedRuleSource.length} + hasActiveFilters={customizationStatus != null} + numActiveFilters={customizationStatus != null ? 1 : 0} data-test-subj="rule-customization-filter-popover-button" > {i18n.RULE_SOURCE} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts deleted file mode 100644 index f0a818fd2532e..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts +++ /dev/null @@ -1,48 +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 { useMemo } from 'react'; -import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; -import { RuleCustomizationEnum, type FilterOptions } from '../../../../rule_management/logic/types'; - -export type UpgradePrebuiltRulesTableFilterOptions = Pick< - FilterOptions, - 'filter' | 'tags' | 'ruleSource' ->; - -interface UseFilterPrebuiltRulesToUpgradeParams { - data: RuleUpgradeState[]; - filterOptions: UpgradePrebuiltRulesTableFilterOptions; -} - -export const useFilterPrebuiltRulesToUpgrade = ({ - data, - filterOptions, -}: UseFilterPrebuiltRulesToUpgradeParams): RuleUpgradeState[] => { - return useMemo(() => { - const { filter, tags, ruleSource } = filterOptions; - - return data.filter((ruleInfo) => { - if (filter && !ruleInfo.current_rule.name.toLowerCase().includes(filter.toLowerCase())) { - return false; - } - - if (tags?.length && !tags.every((tag) => ruleInfo.current_rule.tags.includes(tag))) { - return false; - } - - if (ruleSource?.length === 1 && ruleInfo.current_rule.rule_source.type === 'external') { - if (ruleSource.includes(RuleCustomizationEnum.customized)) { - return ruleInfo.current_rule.rule_source.is_customized; - } - return ruleInfo.current_rule.rule_source.is_customized === false; - } - - return true; - }); - }, [filterOptions, data]); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/add_elastic_rules_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/add_elastic_rules_button.tsx index d62c080ace66f..e0f87a3ee22cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/add_elastic_rules_button.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/add_elastic_rules_button.tsx @@ -33,7 +33,7 @@ export const AddElasticRulesButton = ({ }); const { data: preBuiltRulesStatus } = usePrebuiltRulesStatus(); - const newRulesCount = preBuiltRulesStatus?.num_prebuilt_rules_to_install ?? 0; + const newRulesCount = preBuiltRulesStatus?.stats.num_prebuilt_rules_to_install ?? 0; const ButtonComponent = fill ? EuiButton : EuiButtonEmpty; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index 52e2c552c74fa..482a50ae45d86 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -55,6 +55,11 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter num_prebuilt_rules_to_upgrade: upgradeableRules.length, num_prebuilt_rules_total_in_package: totalAvailableRules.length, }, + aggregated_fields: { + upgradeable_rules: { + tags: [...new Set(upgradeableRules.flatMap((rule) => rule.current.tags))], + }, + }, }; return response.ok({ body }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts index 750561b9858a9..9dd60a764e3f2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts @@ -12,19 +12,26 @@ import type { } from '../../../../../../common/api/detection_engine'; import { ModeEnum, SkipRuleUpgradeReasonEnum } from '../../../../../../common/api/detection_engine'; import type { PromisePoolError } from '../../../../../utils/promise_pool'; -import type { Mode } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { UpgradeConflictResolutionEnum } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { + Mode, + UpgradeConflictResolution, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; +import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; export const getUpgradeableRules = ({ rawUpgradeableRules, currentRules, versionSpecifiers, mode, + onConflict = UpgradeConflictResolutionEnum.OVERWRITE, }: { rawUpgradeableRules: RuleTriad[]; currentRules: RuleResponse[]; versionSpecifiers?: RuleUpgradeSpecifier[]; mode: Mode; + onConflict?: UpgradeConflictResolution; }) => { return withSecuritySpanSync(getUpgradeableRules.name, () => { const upgradeableRules = new Map( @@ -76,6 +83,20 @@ export const getUpgradeableRules = ({ }); } + if (onConflict === UpgradeConflictResolutionEnum.SKIP) { + rawUpgradeableRules.forEach(({ current, base, target }) => { + const ruleDiff = calculateRuleDiff({ current, base, target }); + const hasConflict = ruleDiff.ruleDiff.num_fields_with_conflicts > 0; + if (hasConflict) { + skippedRules.push({ + rule_id: current.rule_id, + reason: SkipRuleUpgradeReasonEnum.CONFLICT, + }); + upgradeableRules.delete(current.rule_id); + } + }); + } + return { upgradeableRules: Array.from(upgradeableRules.values()), fetchErrors, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts index 2c10d62d570db..9a42f58f3206e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -31,6 +31,7 @@ import { createModifiedPrebuiltRuleAssets } from './create_upgradeable_rules_pay import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; import { validatePerformRuleUpgradeRequest } from './validate_perform_rule_upgrade_request'; import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -80,22 +81,28 @@ export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => defaultPickVersion, }); - const { mode } = request.body; + const { mode, dry_run: isDryRun, on_conflict: onConflict } = request.body; const versionSpecifiers = mode === ModeEnum.ALL_RULES ? undefined : request.body.rules; + const filter = mode === ModeEnum.ALL_RULES ? request.body.filter : undefined; const ruleTriadsMap = await fetchRuleVersionsTriad({ ruleAssetsClient, ruleObjectsClient, versionSpecifiers, + filter, }); const ruleGroups = getRuleGroups(ruleTriadsMap); + const ruleErrors = []; + const { upgradeableRules, skippedRules, fetchErrors } = getUpgradeableRules({ rawUpgradeableRules: ruleGroups.upgradeableRules, currentRules: ruleGroups.currentRules, versionSpecifiers, mode, + onConflict, }); + ruleErrors.push(...fetchErrors); const { modifiedPrebuiltRuleAssets, processingErrors } = createModifiedPrebuiltRuleAssets( { @@ -104,12 +111,32 @@ export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => defaultPickVersion, } ); + ruleErrors.push(...processingErrors); + + if (isDryRun) { + const body: PerformRuleUpgradeResponseBody = { + summary: { + total: upgradeableRules.length + skippedRules.length, + skipped: skippedRules.length, + succeeded: modifiedPrebuiltRuleAssets.length, + failed: ruleErrors.length, + }, + results: { + updated: modifiedPrebuiltRuleAssets.map((rule) => + convertPrebuiltRuleAssetToRuleResponse(rule) + ), + skipped: skippedRules, + }, + errors: aggregatePrebuiltRuleErrors(ruleErrors), + }; + return response.ok({ body }); + } const { results: updatedRules, errors: installationErrors } = await upgradePrebuiltRules( detectionRulesClient, modifiedPrebuiltRuleAssets ); - const ruleErrors = [...fetchErrors, ...processingErrors, ...installationErrors]; + ruleErrors.push(...installationErrors); const { error: timelineInstallationError } = await performTimelinesInstallation( ctx.securitySolution diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts new file mode 100644 index 0000000000000..1bfd9f09c43f7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts @@ -0,0 +1,59 @@ +/* + * 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 { pickBy } from 'lodash'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { + RuleUpgradeInfoForReview, + ThreeWayDiff, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { ThreeWayDiffOutcome } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; +import type { CalculateRuleDiffResult } from '../../logic/diff/calculate_rule_diff'; + +export const calculateRuleUpgradeInfo = ( + results: CalculateRuleDiffResult[] +): RuleUpgradeInfoForReview[] => { + return results.map((result) => { + const { ruleDiff, ruleVersions } = result; + const installedCurrentVersion = ruleVersions.input.current; + const targetVersion = ruleVersions.input.target; + invariant(installedCurrentVersion != null, 'installedCurrentVersion not found'); + invariant(targetVersion != null, 'targetVersion not found'); + + const targetRule: RuleResponse = { + ...convertPrebuiltRuleAssetToRuleResponse(targetVersion), + id: installedCurrentVersion.id, + revision: installedCurrentVersion.revision + 1, + created_at: installedCurrentVersion.created_at, + created_by: installedCurrentVersion.created_by, + updated_at: new Date().toISOString(), + updated_by: installedCurrentVersion.updated_by, + }; + + return { + id: installedCurrentVersion.id, + rule_id: installedCurrentVersion.rule_id, + revision: installedCurrentVersion.revision, + version: installedCurrentVersion.version, + current_rule: installedCurrentVersion, + target_rule: targetRule, + diff: { + fields: pickBy>( + ruleDiff.fields, + (fieldDiff) => + fieldDiff.diff_outcome !== ThreeWayDiffOutcome.StockValueNoUpdate && + fieldDiff.diff_outcome !== ThreeWayDiffOutcome.MissingBaseNoUpdate + ), + num_fields_with_updates: ruleDiff.num_fields_with_updates, + num_fields_with_conflicts: ruleDiff.num_fields_with_conflicts, + num_fields_with_non_solvable_conflicts: ruleDiff.num_fields_with_non_solvable_conflicts, + }, + }; + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts index 9f4fa7ddc766e..ae6a6cbde2811 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts @@ -7,32 +7,36 @@ import type { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { pickBy } from 'lodash'; import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; import type { + PrebuiltRuleFilter, + ReviewRuleUpgradeRequestBody, ReviewRuleUpgradeResponseBody, - RuleUpgradeInfoForReview, - RuleUpgradeStatsForReview, - ThreeWayDiff, + ReviewRuleUpgradeSort, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import { ThreeWayDiffOutcome } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import { invariant } from '../../../../../../common/utils/invariant'; import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; import { buildSiemResponse } from '../../../routes/utils'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; -import type { CalculateRuleDiffResult } from '../../logic/diff/calculate_rule_diff'; import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; +import type { IPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +import type { RuleVersionSpecifier } from '../../logic/rule_versions/rule_version_specifier'; +import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; +import { calculateRuleUpgradeInfo } from './calculate_rule_upgrade_info'; +import type { IPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; + +const DEFAULT_SORT: ReviewRuleUpgradeSort = { + field: 'name', + order: 'asc', +}; export const reviewRuleUpgradeHandler = async ( context: SecuritySolutionRequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) => { const siemResponse = buildSiemResponse(response); + const { page = 1, per_page: perPage = 10_000, sort = DEFAULT_SORT, filter } = request.body ?? {}; try { const ctx = await context.resolve(['core', 'alerting']); @@ -41,21 +45,26 @@ export const reviewRuleUpgradeHandler = async ( const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const ruleVersionsMap = await fetchRuleVersionsTriad({ + const { diffResults, totalUpgradeableRules } = await calculateUpgradeableRulesDiff({ ruleAssetsClient, ruleObjectsClient, - }); - const { upgradeableRules } = getRuleGroups(ruleVersionsMap); - - const ruleDiffCalculationResults = upgradeableRules.map(({ current }) => { - const ruleVersions = ruleVersionsMap.get(current.rule_id); - invariant(ruleVersions != null, 'ruleVersions not found'); - return calculateRuleDiff(ruleVersions); + page, + perPage, + sort, + filter, }); const body: ReviewRuleUpgradeResponseBody = { - stats: calculateRuleStats(ruleDiffCalculationResults), - rules: calculateRuleInfos(ruleDiffCalculationResults), + stats: { + num_rules_to_upgrade_total: 0, + num_rules_with_conflicts: 0, + num_rules_with_non_solvable_conflicts: 0, + tags: [], + }, + rules: calculateRuleUpgradeInfo(diffResults), + page, + per_page: perPage, + total: totalUpgradeableRules, }; return response.ok({ body }); @@ -67,72 +76,80 @@ export const reviewRuleUpgradeHandler = async ( }); } }; -const calculateRuleStats = (results: CalculateRuleDiffResult[]): RuleUpgradeStatsForReview => { - const allTags = new Set(); - const stats = results.reduce( - (acc, result) => { - acc.num_rules_to_upgrade_total += 1; +interface CalculateUpgradeableRulesDiffArgs { + ruleAssetsClient: IPrebuiltRuleAssetsClient; + ruleObjectsClient: IPrebuiltRuleObjectsClient; + page: number; + perPage: number; + sort: ReviewRuleUpgradeSort; + filter: PrebuiltRuleFilter | undefined; +} - if (result.ruleDiff.num_fields_with_conflicts > 0) { - acc.num_rules_with_conflicts += 1; - } +const BATCH_SIZE = 100; - if (result.ruleDiff.num_fields_with_non_solvable_conflicts > 0) { - acc.num_rules_with_non_solvable_conflicts += 1; - } +async function calculateUpgradeableRulesDiff({ + ruleAssetsClient, + ruleObjectsClient, + page, + perPage, + sort, + filter, +}: CalculateUpgradeableRulesDiffArgs) { + const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); + const latestVersionsMap = new Map(allLatestVersions.map((version) => [version.rule_id, version])); + + const upgradeableRules: Array<{ rule: RuleResponse; targetVersion: RuleVersionSpecifier }> = []; + let foundUpgradeableRules = 0; - result.ruleVersions.input.current?.tags.forEach((tag) => allTags.add(tag)); + // Fetch all installed rules that have a newer version available in batches to + // avoid loading all rules at once into memory. We need to iterate over all + // rules, even if the perPage limit is reached, to calculate the total number + // of upgradeable rules and all tags. + let batchPage = 1; + while (true) { + const currentRulesBatch = await ruleObjectsClient.fetchInstalledRules({ + filter, + perPage: BATCH_SIZE, + page: batchPage, + sortField: sort.field, + sortOrder: sort.order, + }); - return acc; - }, - { - num_rules_to_upgrade_total: 0, - num_rules_with_conflicts: 0, - num_rules_with_non_solvable_conflicts: 0, + if (currentRulesBatch.length === 0) { + break; } + currentRulesBatch.forEach((rule) => { + const targetVersion = latestVersionsMap.get(rule.rule_id); + if (targetVersion != null && rule.version < targetVersion.version) { + // Push the rule to the list of upgradeable rules if it falls within the current page + if (foundUpgradeableRules >= (page - 1) * perPage && upgradeableRules.length < perPage) { + upgradeableRules.push({ rule, targetVersion }); + } + foundUpgradeableRules += 1; + } + }); + batchPage += 1; + } + + // Zip current rules with their base and target versions + const currentRules = upgradeableRules.map(({ rule }) => rule); + const latestRules = await ruleAssetsClient.fetchAssetsByVersion( + upgradeableRules.map(({ targetVersion }) => targetVersion) ); + const baseRules = await ruleAssetsClient.fetchAssetsByVersion(currentRules); + const ruleVersionsMap = zipRuleVersions(currentRules, baseRules, latestRules); + + // Calculate the diff between current, base, and target versions + // Iterate through the current rules array to keep the order of the results + const diffResults = currentRules.map((current) => { + const base = ruleVersionsMap.get(current.rule_id)?.base; + const target = ruleVersionsMap.get(current.rule_id)?.target; + return calculateRuleDiff({ current, base, target }); + }); return { - ...stats, - tags: Array.from(allTags), + diffResults, + totalUpgradeableRules: foundUpgradeableRules, }; -}; -const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfoForReview[] => { - return results.map((result) => { - const { ruleDiff, ruleVersions } = result; - const installedCurrentVersion = ruleVersions.input.current; - const targetVersion = ruleVersions.input.target; - invariant(installedCurrentVersion != null, 'installedCurrentVersion not found'); - invariant(targetVersion != null, 'targetVersion not found'); - - const targetRule: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(targetVersion), - id: installedCurrentVersion.id, - revision: installedCurrentVersion.revision + 1, - created_at: installedCurrentVersion.created_at, - created_by: installedCurrentVersion.created_by, - updated_at: new Date().toISOString(), - updated_by: installedCurrentVersion.updated_by, - }; - - return { - id: installedCurrentVersion.id, - rule_id: installedCurrentVersion.rule_id, - revision: installedCurrentVersion.revision, - current_rule: installedCurrentVersion, - target_rule: targetRule, - diff: { - fields: pickBy>( - ruleDiff.fields, - (fieldDiff) => - fieldDiff.diff_outcome !== ThreeWayDiffOutcome.StockValueNoUpdate && - fieldDiff.diff_outcome !== ThreeWayDiffOutcome.MissingBaseNoUpdate - ), - num_fields_with_updates: ruleDiff.num_fields_with_updates, - num_fields_with_conflicts: ruleDiff.num_fields_with_conflicts, - num_fields_with_non_solvable_conflicts: ruleDiff.num_fields_with_non_solvable_conflicts, - }, - }; - }); -}; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index 1138a48cc39d4..04b69acacea69 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -12,11 +12,24 @@ import type { } from '../../../../../../common/api/detection_engine/model/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/logic/detection_rules_client/converters/internal_rule_to_api_response'; +import { convertRulesFilterToKQL } from '../../../../../../common/detection_engine/rule_management/rule_filtering'; +import type { + FindRulesSortField, + PrebuiltRuleFilter, + SortOrder, +} from '../../../../../../common/api/detection_engine'; + +interface FetchAllInstalledRulesArgs { + page?: number; + perPage?: number; + filter?: PrebuiltRuleFilter; + sortField?: FindRulesSortField; + sortOrder?: SortOrder; +} export interface IPrebuiltRuleObjectsClient { - fetchAllInstalledRules(): Promise; + fetchInstalledRules(args?: FetchAllInstalledRulesArgs): Promise; fetchInstalledRulesByIds(ruleIds: string[]): Promise; } @@ -24,10 +37,28 @@ export const createPrebuiltRuleObjectsClient = ( rulesClient: RulesClient ): IPrebuiltRuleObjectsClient => { return { - fetchAllInstalledRules: (): Promise => { + fetchInstalledRules: ({ page, perPage, sortField, sortOrder, filter } = {}): Promise< + RuleResponse[] + > => { return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRules', async () => { - const rulesData = await getExistingPrepackagedRules({ rulesClient }); - const rules = rulesData.map((rule) => internalRuleToAPIResponse(rule)); + const filterKQL = convertRulesFilterToKQL({ + showElasticRules: true, + filter: filter?.name, + tags: filter?.tags, + customizationStatus: filter?.customization_status, + }); + + const rulesData = await findRules({ + rulesClient, + ruleIds: filter?.rule_ids, + filter: filterKQL, + perPage, + page, + sortField, + sortOrder, + fields: undefined, + }); + const rules = rulesData.data.map((rule) => internalRuleToAPIResponse(rule)); return rules; }); }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts index 11a5660e77a31..f818a08683522 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { PrebuiltRuleFilter } from '../../../../../../common/api/detection_engine'; +import { MAX_PREBUILT_RULES_COUNT } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; 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'; @@ -15,19 +17,25 @@ interface GetRuleVersionsMapArgs { ruleObjectsClient: IPrebuiltRuleObjectsClient; ruleAssetsClient: IPrebuiltRuleAssetsClient; versionSpecifiers?: RuleVersionSpecifier[]; + filter?: PrebuiltRuleFilter; } export async function fetchRuleVersionsTriad({ ruleObjectsClient, ruleAssetsClient, versionSpecifiers, + filter, }: GetRuleVersionsMapArgs): Promise> { const [currentRules, latestRules] = await Promise.all([ versionSpecifiers ? ruleObjectsClient.fetchInstalledRulesByIds( versionSpecifiers.map(({ rule_id: ruleId }) => ruleId) ) - : ruleObjectsClient.fetchAllInstalledRules(), + : ruleObjectsClient.fetchInstalledRules({ + filter, + page: 1, + perPage: MAX_PREBUILT_RULES_COUNT, + }), versionSpecifiers ? ruleAssetsClient.fetchAssetsByVersion(versionSpecifiers) : ruleAssetsClient.fetchLatestAssets(), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts index 24b2954547e40..9a7eb8fdf6f56 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts @@ -49,16 +49,20 @@ export const getRulesCount = async ({ export const getRules = async ({ rulesClient, filter, + page = 1, + perPage = MAX_PREBUILT_RULES_COUNT, }: { rulesClient: RulesClient; filter: string; + page?: number; + perPage?: number; }): Promise => withSecuritySpan('getRules', async () => { const rules = await findRules({ rulesClient, filter, - perPage: MAX_PREBUILT_RULES_COUNT, - page: 1, + perPage, + page, sortField: 'createdAt', sortOrder: 'desc', fields: undefined, @@ -80,11 +84,17 @@ export const getNonPackagedRules = async ({ export const getExistingPrepackagedRules = async ({ rulesClient, + page, + perPage, }: { rulesClient: RulesClient; + page?: number; + perPage?: number; }): Promise => { return getRules({ rulesClient, + page, + perPage, filter: KQL_FILTER_IMMUTABLE_RULES, }); }; From e5ed827f46732741f79edac88daab3a460dc6ac4 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Thu, 13 Feb 2025 17:50:39 +0100 Subject: [PATCH 03/18] Reduce the _review rule upgrade endpoint response size --- ...tch_prebuilt_rules_install_review_query.ts | 4 -- .../use_prebuilt_rules_upgrade_state.test.ts | 1 + .../get_prebuilt_rules_status_route.ts | 30 ++++++--- .../review_rule_upgrade_handler.ts | 63 +++++++------------ .../review_rule_upgrade_route.ts | 7 +-- .../prebuilt_rule_objects_client.ts | 56 ++++++++++++++--- .../fetch_rule_versions_triad.ts | 6 +- 7 files changed, 97 insertions(+), 70 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts index 9d88e8984962f..0f7f5b512b0fa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts @@ -11,8 +11,6 @@ import { reviewRuleInstall } from '../../api'; import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; import type { ReviewRuleInstallationResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; -import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; -import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const REVIEW_RULE_INSTALLATION_QUERY_KEY = ['POST', REVIEW_RULE_INSTALLATION_URL]; @@ -28,8 +26,6 @@ export const useFetchPrebuiltRulesInstallReviewQuery = ( { ...DEFAULT_QUERY_OPTIONS, ...options, - retry: retryOnRateLimitedError, - retryDelay: cappedExponentialBackoff, } ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts index 28b24a4efe9a5..3a35e783975e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts @@ -429,6 +429,7 @@ function createRuleUpgradeInfoMock( num_fields_with_non_solvable_conflicts: 0, fields: {}, }, + version: 1, revision: 1, ...rewrites, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index 482a50ae45d86..976cea495679d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -12,8 +12,6 @@ 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 { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -41,19 +39,33 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const ruleVersionsMap = await fetchRuleVersionsTriad({ - ruleAssetsClient, - ruleObjectsClient, + const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); + const latestRuleVersions = await ruleAssetsClient.fetchLatestVersions(); + const currentRuleVersionsMap = new Map( + currentRuleVersions.map((rule) => [rule.rule_id, rule]) + ); + const latestRuleVersionsMap = new Map( + latestRuleVersions.map((rule) => [rule.rule_id, rule]) + ); + const installableRules = latestRuleVersions.filter( + (rule) => !currentRuleVersionsMap.has(rule.rule_id) + ); + const upgradeableRules = currentRuleVersions.filter((rule) => { + const latestVersion = latestRuleVersionsMap.get(rule.rule_id); + return latestVersion != null && rule.version < latestVersion.version; }); - const { currentRules, installableRules, upgradeableRules, totalAvailableRules } = - getRuleGroups(ruleVersionsMap); const body: GetPrebuiltRulesStatusResponseBody = { stats: { - num_prebuilt_rules_installed: currentRules.length, + num_prebuilt_rules_installed: currentRuleVersions.length, num_prebuilt_rules_to_install: installableRules.length, num_prebuilt_rules_to_upgrade: upgradeableRules.length, - num_prebuilt_rules_total_in_package: totalAvailableRules.length, + num_prebuilt_rules_total_in_package: latestRuleVersions.length, + }, + aggregated_fields: { + upgradeable_rules: { + tags: [...new Set(upgradeableRules.flatMap((rule) => rule.tags))], + }, }, aggregated_fields: { upgradeable_rules: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts index ae6a6cbde2811..8f1a9010dcd69 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts @@ -7,7 +7,6 @@ import type { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; import type { PrebuiltRuleFilter, ReviewRuleUpgradeRequestBody, @@ -19,11 +18,11 @@ import { buildSiemResponse } from '../../../routes/utils'; import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; import type { IPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +import type { IPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; +import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import type { RuleVersionSpecifier } from '../../logic/rule_versions/rule_version_specifier'; import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; import { calculateRuleUpgradeInfo } from './calculate_rule_upgrade_info'; -import type { IPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; const DEFAULT_SORT: ReviewRuleUpgradeSort = { field: 'name', @@ -86,8 +85,6 @@ interface CalculateUpgradeableRulesDiffArgs { filter: PrebuiltRuleFilter | undefined; } -const BATCH_SIZE = 100; - async function calculateUpgradeableRulesDiff({ ruleAssetsClient, ruleObjectsClient, @@ -99,43 +96,27 @@ async function calculateUpgradeableRulesDiff({ const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); const latestVersionsMap = new Map(allLatestVersions.map((version) => [version.rule_id, version])); - const upgradeableRules: Array<{ rule: RuleResponse; targetVersion: RuleVersionSpecifier }> = []; - let foundUpgradeableRules = 0; - - // Fetch all installed rules that have a newer version available in batches to - // avoid loading all rules at once into memory. We need to iterate over all - // rules, even if the perPage limit is reached, to calculate the total number - // of upgradeable rules and all tags. - let batchPage = 1; - while (true) { - const currentRulesBatch = await ruleObjectsClient.fetchInstalledRules({ - filter, - perPage: BATCH_SIZE, - page: batchPage, - sortField: sort.field, - sortOrder: sort.order, - }); - - if (currentRulesBatch.length === 0) { - break; - } - currentRulesBatch.forEach((rule) => { + const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions({ + filter, + sortField: sort.field, + sortOrder: sort.order, + }); + const upgradeableRuleIds = currentRuleVersions + .filter((rule) => { const targetVersion = latestVersionsMap.get(rule.rule_id); - if (targetVersion != null && rule.version < targetVersion.version) { - // Push the rule to the list of upgradeable rules if it falls within the current page - if (foundUpgradeableRules >= (page - 1) * perPage && upgradeableRules.length < perPage) { - upgradeableRules.push({ rule, targetVersion }); - } - foundUpgradeableRules += 1; - } - }); - batchPage += 1; - } - - // Zip current rules with their base and target versions - const currentRules = upgradeableRules.map(({ rule }) => rule); + return targetVersion != null && rule.version < targetVersion.version; + }) + .map((rule) => rule.rule_id); + const totalUpgradeableRules = upgradeableRuleIds.length; + + const pagedRuleIds = upgradeableRuleIds.slice((page - 1) * perPage, page * perPage); + const currentRules = await ruleObjectsClient.fetchInstalledRulesByIds({ + ruleIds: pagedRuleIds, + sortField: sort.field, + sortOrder: sort.order, + }); const latestRules = await ruleAssetsClient.fetchAssetsByVersion( - upgradeableRules.map(({ targetVersion }) => targetVersion) + currentRules.map(({ rule_id: ruleId }) => latestVersionsMap.get(ruleId) as RuleVersionSpecifier) ); const baseRules = await ruleAssetsClient.fetchAssetsByVersion(currentRules); const ruleVersionsMap = zipRuleVersions(currentRules, baseRules, latestRules); @@ -150,6 +131,6 @@ async function calculateUpgradeableRulesDiff({ return { diffResults, - totalUpgradeableRules: foundUpgradeableRules, + totalUpgradeableRules, }; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index 94ee1282fae98..a2098dfdc21be 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -11,11 +11,7 @@ import { ReviewRuleUpgradeRequestBody, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; -import { - PREBUILT_RULES_OPERATION_CONCURRENCY, - PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, -} from '../../constants'; +import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; import { reviewRuleUpgradeHandler } from './review_rule_upgrade_handler'; export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { @@ -29,7 +25,6 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => }, }, options: { - tags: [routeLimitedConcurrencyTag(PREBUILT_RULES_OPERATION_CONCURRENCY)], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index 04b69acacea69..b2b6367303d6c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -9,6 +9,7 @@ import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { RuleResponse, RuleSignatureId, + RuleTagArray, } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { withSecuritySpan } from '../../../../../utils/with_security_span'; import { findRules } from '../../../rule_management/logic/search/find_rules'; @@ -19,6 +20,8 @@ import type { PrebuiltRuleFilter, SortOrder, } from '../../../../../../common/api/detection_engine'; +import { MAX_PREBUILT_RULES_COUNT } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; +import type { RuleVersionSpecifier } from '../rule_versions/rule_version_specifier'; interface FetchAllInstalledRulesArgs { page?: number; @@ -28,18 +31,31 @@ interface FetchAllInstalledRulesArgs { sortOrder?: SortOrder; } +interface FetchAllInstalledRuleVersionsArgs { + filter?: PrebuiltRuleFilter; + sortField?: FindRulesSortField; + sortOrder?: SortOrder; +} + +interface FetchInstalledRulesByIdsArgs { + ruleIds: RuleSignatureId[]; + sortField?: FindRulesSortField; + sortOrder?: SortOrder; +} + export interface IPrebuiltRuleObjectsClient { fetchInstalledRules(args?: FetchAllInstalledRulesArgs): Promise; - fetchInstalledRulesByIds(ruleIds: string[]): Promise; + fetchInstalledRuleVersions( + args?: FetchAllInstalledRuleVersionsArgs + ): Promise>; + fetchInstalledRulesByIds(args: FetchInstalledRulesByIdsArgs): Promise; } export const createPrebuiltRuleObjectsClient = ( rulesClient: RulesClient ): IPrebuiltRuleObjectsClient => { return { - fetchInstalledRules: ({ page, perPage, sortField, sortOrder, filter } = {}): Promise< - RuleResponse[] - > => { + fetchInstalledRules: ({ page, perPage, sortField, sortOrder, filter } = {}) => { return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRules', async () => { const filterKQL = convertRulesFilterToKQL({ showElasticRules: true, @@ -62,14 +78,40 @@ export const createPrebuiltRuleObjectsClient = ( return rules; }); }, - fetchInstalledRulesByIds: (ruleIds: RuleSignatureId[]): Promise => { + fetchInstalledRuleVersions: ({ filter, sortField, sortOrder } = {}) => { + return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRuleVersions', async () => { + const filterKQL = convertRulesFilterToKQL({ + showElasticRules: true, + filter: filter?.name, + tags: filter?.tags, + customizationStatus: filter?.customization_status, + }); + + const rulesData = await findRules({ + rulesClient, + ruleIds: filter?.rule_ids, + filter: filterKQL, + perPage: MAX_PREBUILT_RULES_COUNT, + page: 1, + sortField, + sortOrder, + fields: ['params.ruleId', 'params.version', 'tags'], + }); + return rulesData.data.map((rule) => ({ + rule_id: rule.params.ruleId, + version: rule.params.version, + tags: rule.tags, + })); + }); + }, + fetchInstalledRulesByIds: ({ ruleIds, sortField = 'createdAt', sortOrder = 'desc' }) => { return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRulesByIds', async () => { const { data } = await findRules({ rulesClient, perPage: ruleIds.length, page: 1, - sortField: 'createdAt', - sortOrder: 'desc', + sortField, + sortOrder, fields: undefined, filter: `alert.attributes.params.ruleId:(${ruleIds.join(' or ')})`, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts index f818a08683522..c5b2ee4f542de 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts @@ -28,9 +28,9 @@ export async function fetchRuleVersionsTriad({ }: GetRuleVersionsMapArgs): Promise> { const [currentRules, latestRules] = await Promise.all([ versionSpecifiers - ? ruleObjectsClient.fetchInstalledRulesByIds( - versionSpecifiers.map(({ rule_id: ruleId }) => ruleId) - ) + ? ruleObjectsClient.fetchInstalledRulesByIds({ + ruleIds: versionSpecifiers.map(({ rule_id: ruleId }) => ruleId), + }) : ruleObjectsClient.fetchInstalledRules({ filter, page: 1, From 07b4d6678516db640f2fa1584f381f89d69264c2 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Fri, 21 Feb 2025 11:51:00 -0500 Subject: [PATCH 04/18] fixes merge --- .../add_prebuilt_rules_table/add_prebuilt_rules_table.tsx | 2 +- .../upgrade_prebuilt_rules_table.tsx | 2 +- .../get_prebuilt_rules_status_route.ts | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx index 1baf002926d68..db4a7bc39e6c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx @@ -78,7 +78,7 @@ export const AddPrebuiltRulesTable = React.memo(() => { ) : ( <> - + diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx index 959f20ef72c20..d17fddf37c058 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx @@ -97,7 +97,7 @@ export const UpgradePrebuiltRulesTable = React.memo(() => { ) : ( <> - + diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index 976cea495679d..adccc7ea6488d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -67,11 +67,6 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter tags: [...new Set(upgradeableRules.flatMap((rule) => rule.tags))], }, }, - aggregated_fields: { - upgradeable_rules: { - tags: [...new Set(upgradeableRules.flatMap((rule) => rule.current.tags))], - }, - }, }; return response.ok({ body }); From a1d7e8c4bf018effb43774189acb0e5bac2245a5 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Thu, 13 Feb 2025 17:50:39 +0100 Subject: [PATCH 05/18] Reduce the _review rule upgrade endpoint response size --- ...tch_prebuilt_rules_upgrade_review_query.ts | 4 - .../get_upgradeable_rules.test.ts | 191 -------------- .../get_upgradeable_rules.ts | 106 -------- .../perform_rule_upgrade_handler.ts | 235 +++++++++++++++++ .../perform_rule_upgrade_route.ts | 136 +--------- .../review_rule_installation_handler.ts | 20 +- .../prebuilt_rule_objects_client.ts | 4 + .../preview_prebuilt_rules_upgrade.ts | 236 ------------------ 8 files changed, 255 insertions(+), 677 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.test.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts index 4b779918febc3..3ff49ab1b9161 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts @@ -14,8 +14,6 @@ import type { ReviewRuleUpgradeResponseBody, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; -import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; -import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const REVIEW_RULE_UPGRADE_QUERY_KEY = ['POST', REVIEW_RULE_UPGRADE_URL]; @@ -32,8 +30,6 @@ export const useFetchPrebuiltRulesUpgradeReviewQuery = ( { ...DEFAULT_QUERY_OPTIONS, ...options, - retry: retryOnRateLimitedError, - retryDelay: cappedExponentialBackoff, } ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.test.ts deleted file mode 100644 index 5b1c74825102c..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.test.ts +++ /dev/null @@ -1,191 +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 { getUpgradeableRules } from './get_upgradeable_rules'; -import { ModeEnum, SkipRuleUpgradeReasonEnum } from '../../../../../../common/api/detection_engine'; -import type { - RuleResponse, - RuleUpgradeSpecifier, -} from '../../../../../../common/api/detection_engine'; -import { getPrebuiltRuleMockOfType } from '../../model/rule_assets/prebuilt_rule_asset.mock'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; -import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; - -describe('getUpgradeableRules', () => { - const baseRule = getPrebuiltRuleMockOfType('query'); - const createUpgradeableRule = ( - ruleId: string, - currentVersion: number, - targetVersion: number - ): RuleTriad => { - return { - current: { - ...baseRule, - rule_id: ruleId, - version: currentVersion, - revision: 0, - }, - target: { ...baseRule, rule_id: ruleId, version: targetVersion }, - } as RuleTriad; - }; - - const mockUpgradeableRule = createUpgradeableRule('rule-1', 1, 2); - - const mockCurrentRule: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(baseRule), - rule_id: 'rule-1', - revision: 0, - version: 1, - }; - - describe('ALL_RULES mode', () => { - it('should return all upgradeable rules when in ALL_RULES mode', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule], - mode: ModeEnum.ALL_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule]); - expect(result.fetchErrors).toEqual([]); - expect(result.skippedRules).toEqual([]); - }); - - it('should handle empty upgradeable rules list', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [], - currentRules: [], - mode: ModeEnum.ALL_RULES, - }); - - expect(result.upgradeableRules).toEqual([]); - expect(result.fetchErrors).toEqual([]); - expect(result.skippedRules).toEqual([]); - }); - }); - - describe('SPECIFIC_RULES mode', () => { - const mockVersionSpecifier: RuleUpgradeSpecifier = { - rule_id: 'rule-1', - revision: 0, - version: 1, - }; - - it('should return specified upgradeable rules when in SPECIFIC_RULES mode', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule], - versionSpecifiers: [mockVersionSpecifier], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule]); - expect(result.fetchErrors).toEqual([]); - expect(result.skippedRules).toEqual([]); - }); - - it('should handle rule not found', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule], - versionSpecifiers: [{ ...mockVersionSpecifier, rule_id: 'nonexistent' }], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule]); - expect(result.fetchErrors).toHaveLength(1); - expect(result.fetchErrors[0].error.message).toContain( - 'Rule with rule_id "nonexistent" and version "1" not found' - ); - expect(result.skippedRules).toEqual([]); - }); - - it('should handle non-upgradeable rule', () => { - const nonUpgradeableRule: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(baseRule), - rule_id: 'rule-2', - revision: 0, - version: 1, - }; - - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule, nonUpgradeableRule], - versionSpecifiers: [mockVersionSpecifier, { ...mockVersionSpecifier, rule_id: 'rule-2' }], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule]); - expect(result.fetchErrors).toEqual([]); - expect(result.skippedRules).toEqual([ - { rule_id: 'rule-2', reason: SkipRuleUpgradeReasonEnum.RULE_UP_TO_DATE }, - ]); - }); - - it('should handle revision mismatch', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule], - versionSpecifiers: [{ ...mockVersionSpecifier, revision: 1 }], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([]); - expect(result.fetchErrors).toHaveLength(1); - expect(result.fetchErrors[0].error.message).toContain( - 'Revision mismatch for rule_id rule-1: expected 0, got 1' - ); - expect(result.skippedRules).toEqual([]); - }); - - it('should handle multiple rules with mixed scenarios', () => { - const mockUpgradeableRule2 = createUpgradeableRule('rule-2', 1, 2); - const mockCurrentRule2: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(baseRule), - rule_id: 'rule-2', - revision: 0, - version: 1, - }; - const mockCurrentRule3: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(baseRule), - rule_id: 'rule-3', - revision: 1, - version: 1, - }; - - const result = getUpgradeableRules({ - rawUpgradeableRules: [ - mockUpgradeableRule, - mockUpgradeableRule2, - createUpgradeableRule('rule-3', 1, 2), - ], - currentRules: [mockCurrentRule, mockCurrentRule2, mockCurrentRule3], - versionSpecifiers: [ - mockVersionSpecifier, - { ...mockVersionSpecifier, rule_id: 'rule-2' }, - { ...mockVersionSpecifier, rule_id: 'rule-3', revision: 0 }, - { ...mockVersionSpecifier, rule_id: 'rule-4' }, - { ...mockVersionSpecifier, rule_id: 'rule-5', revision: 1 }, - ], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule, mockUpgradeableRule2]); - expect(result.fetchErrors).toHaveLength(3); - expect(result.fetchErrors[0].error.message).toContain( - 'Revision mismatch for rule_id rule-3: expected 1, got 0' - ); - expect(result.fetchErrors[1].error.message).toContain( - 'Rule with rule_id "rule-4" and version "1" not found' - ); - expect(result.fetchErrors[2].error.message).toContain( - 'Rule with rule_id "rule-5" and version "1" not found' - ); - expect(result.skippedRules).toEqual([]); - }); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts deleted file mode 100644 index 9dd60a764e3f2..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts +++ /dev/null @@ -1,106 +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 { withSecuritySpanSync } from '../../../../../utils/with_security_span'; -import type { - RuleResponse, - RuleUpgradeSpecifier, - SkippedRuleUpgrade, -} from '../../../../../../common/api/detection_engine'; -import { ModeEnum, SkipRuleUpgradeReasonEnum } from '../../../../../../common/api/detection_engine'; -import type { PromisePoolError } from '../../../../../utils/promise_pool'; -import { UpgradeConflictResolutionEnum } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { - Mode, - UpgradeConflictResolution, -} from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; -import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; - -export const getUpgradeableRules = ({ - rawUpgradeableRules, - currentRules, - versionSpecifiers, - mode, - onConflict = UpgradeConflictResolutionEnum.OVERWRITE, -}: { - rawUpgradeableRules: RuleTriad[]; - currentRules: RuleResponse[]; - versionSpecifiers?: RuleUpgradeSpecifier[]; - mode: Mode; - onConflict?: UpgradeConflictResolution; -}) => { - return withSecuritySpanSync(getUpgradeableRules.name, () => { - const upgradeableRules = new Map( - rawUpgradeableRules.map((_rule) => [_rule.current.rule_id, _rule]) - ); - const fetchErrors: Array> = []; - const skippedRules: SkippedRuleUpgrade[] = []; - - if (mode === ModeEnum.SPECIFIC_RULES) { - const installedRuleIds = new Set(currentRules.map((rule) => rule.rule_id)); - const upgradeableRuleIds = new Set(rawUpgradeableRules.map(({ current }) => current.rule_id)); - versionSpecifiers?.forEach((rule) => { - // Check that the requested rule was found - if (!installedRuleIds.has(rule.rule_id)) { - fetchErrors.push({ - error: new Error( - `Rule with rule_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: SkipRuleUpgradeReasonEnum.RULE_UP_TO_DATE, - }); - return; - } - - // Check that rule revisions match (no update slipped in since the user reviewed the list) - const currentRevision = currentRules.find( - (currentRule) => currentRule.rule_id === rule.rule_id - )?.revision; - if (rule.revision !== currentRevision) { - fetchErrors.push({ - error: new Error( - `Revision mismatch for rule_id ${rule.rule_id}: expected ${currentRevision}, got ${rule.revision}` - ), - item: rule, - }); - // Remove the rule from the list of upgradeable rules - if (upgradeableRules.has(rule.rule_id)) { - upgradeableRules.delete(rule.rule_id); - } - } - }); - } - - if (onConflict === UpgradeConflictResolutionEnum.SKIP) { - rawUpgradeableRules.forEach(({ current, base, target }) => { - const ruleDiff = calculateRuleDiff({ current, base, target }); - const hasConflict = ruleDiff.ruleDiff.num_fields_with_conflicts > 0; - if (hasConflict) { - skippedRules.push({ - rule_id: current.rule_id, - reason: SkipRuleUpgradeReasonEnum.CONFLICT, - }); - upgradeableRules.delete(current.rule_id); - } - }); - } - - return { - upgradeableRules: Array.from(upgradeableRules.values()), - fetchErrors, - skippedRules, - }; - }); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts new file mode 100644 index 0000000000000..6bea2c71123da --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts @@ -0,0 +1,235 @@ +/* + * 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 { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { + PerformRuleUpgradeRequestBody, + PerformRuleUpgradeResponseBody, + SkippedRuleUpgrade, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { + ModeEnum, + PickVersionValuesEnum, + SkipRuleUpgradeReasonEnum, + UpgradeConflictResolutionEnum, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; +import { buildSiemResponse } from '../../../routes/utils'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; +import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors'; +import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; +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 { createModifiedPrebuiltRuleAssets } from './create_upgradeable_rules_payload'; +import { validatePerformRuleUpgradeRequest } from './validate_perform_rule_upgrade_request'; +import type { + RuleResponse, + RuleSignatureId, + RuleVersion, +} from '../../../../../../common/api/detection_engine'; +import type { PromisePoolError } from '../../../../../utils/promise_pool'; +import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; +import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; +import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; + +export const performRuleUpgradeHandler = async ( + context: SecuritySolutionRequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory +) => { + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); + const soClient = ctx.core.savedObjects.client; + const rulesClient = await ctx.alerting.getRulesClient(); + const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient(); + const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + + const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus(); + const defaultPickVersion = isRulesCustomizationEnabled + ? PickVersionValuesEnum.MERGED + : PickVersionValuesEnum.TARGET; + + validatePerformRuleUpgradeRequest({ + isRulesCustomizationEnabled, + payload: request.body, + defaultPickVersion, + }); + + const { mode, dry_run: isDryRun, on_conflict: onConflict } = request.body; + + const filter = mode === ModeEnum.ALL_RULES ? request.body.filter : undefined; + + const skippedRules: SkippedRuleUpgrade[] = []; + const updatedRules: RuleResponse[] = []; + const ruleErrors: Array> = []; + const allErrors: PerformRuleUpgradeResponseBody['errors'] = []; + + const ruleUpgradeQueue: Array<{ + rule_id: RuleSignatureId; + version: RuleVersion; + revision?: number; + }> = []; + + if (mode === ModeEnum.ALL_RULES) { + const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); + const latestVersionsMap = new Map( + allLatestVersions.map((version) => [version.rule_id, version]) + ); + const allCurrentVersions = await ruleObjectsClient.fetchInstalledRuleVersions({ + filter, + }); + + allCurrentVersions.forEach((current) => { + const latest = latestVersionsMap.get(current.rule_id); + if (latest && latest.version > current.version) { + ruleUpgradeQueue.push({ + rule_id: current.rule_id, + version: latest.version, + }); + } + }); + } else if (mode === ModeEnum.SPECIFIC_RULES) { + ruleUpgradeQueue.push(...request.body.rules); + } + + const BATCH_SIZE = 100; + while (ruleUpgradeQueue.length > 0) { + const targetRulesForUpgrade = ruleUpgradeQueue.splice(0, BATCH_SIZE); + + const [currentRules, latestRules] = await Promise.all([ + ruleObjectsClient.fetchInstalledRulesByIds({ + ruleIds: targetRulesForUpgrade.map(({ rule_id: ruleId }) => ruleId), + }), + ruleAssetsClient.fetchAssetsByVersion(targetRulesForUpgrade), + ]); + const baseRules = await ruleAssetsClient.fetchAssetsByVersion(currentRules); + const ruleVersionsMap = zipRuleVersions(currentRules, baseRules, latestRules); + + const upgradeableRules: RuleTriad[] = []; + targetRulesForUpgrade.forEach((targetRule) => { + const ruleVersions = ruleVersionsMap.get(targetRule.rule_id); + + const currentVersion = ruleVersions?.current; + const baseVersion = ruleVersions?.base; + const targetVersion = ruleVersions?.target; + + // Check that the requested rule was found + if (!currentVersion) { + ruleErrors.push({ + error: new Error( + `Rule with rule_id "${targetRule.rule_id}" and version "${targetRule.version}" not found` + ), + item: targetRule, + }); + return; + } + + // Check that the requested rule is upgradeable + if (!targetVersion || targetVersion.version <= currentVersion.version) { + skippedRules.push({ + rule_id: targetRule.rule_id, + reason: SkipRuleUpgradeReasonEnum.RULE_UP_TO_DATE, + }); + return; + } + + // Check that rule revisions match (no update slipped in since the user reviewed the list) + if (targetRule.revision != null && targetRule.revision !== currentVersion.revision) { + ruleErrors.push({ + error: new Error( + `Revision mismatch for rule_id ${targetRule.rule_id}: expected ${currentVersion.revision}, got ${targetRule.revision}` + ), + item: targetRule, + }); + return; + } + + // Check there's no conflicts + if (onConflict === UpgradeConflictResolutionEnum.SKIP) { + const ruleDiff = calculateRuleDiff(ruleVersions); + const hasConflict = ruleDiff.ruleDiff.num_fields_with_conflicts > 0; + if (hasConflict) { + skippedRules.push({ + rule_id: targetRule.rule_id, + reason: SkipRuleUpgradeReasonEnum.CONFLICT, + }); + return; + } + } + + // All checks passed, add to the list of rules to upgrade + upgradeableRules.push({ + current: currentVersion, + base: baseVersion, + target: targetVersion, + }); + }); + + const { modifiedPrebuiltRuleAssets, processingErrors } = createModifiedPrebuiltRuleAssets({ + upgradeableRules, + requestBody: request.body, + defaultPickVersion, + }); + ruleErrors.push(...processingErrors); + + if (isDryRun) { + updatedRules.push( + ...modifiedPrebuiltRuleAssets.map((rule) => convertPrebuiltRuleAssetToRuleResponse(rule)) + ); + } else { + const { results: upgradeResults, errors: installationErrors } = await upgradePrebuiltRules( + detectionRulesClient, + modifiedPrebuiltRuleAssets + ); + ruleErrors.push(...installationErrors); + updatedRules.push(...upgradeResults.map(({ result }) => result)); + } + } + + allErrors.push(...aggregatePrebuiltRuleErrors(ruleErrors)); + + if (!isDryRun) { + const { error: timelineInstallationError } = await performTimelinesInstallation( + ctx.securitySolution + ); + + if (timelineInstallationError) { + allErrors.push({ + message: timelineInstallationError, + rules: [], + }); + } + } + + const body: PerformRuleUpgradeResponseBody = { + summary: { + total: updatedRules.length + skippedRules.length + ruleErrors.length, + skipped: skippedRules.length, + succeeded: updatedRules.length, + failed: ruleErrors.length, + }, + results: { + updated: updatedRules, + skipped: skippedRules, + }, + errors: allErrors, + }; + + return response.ok({ body }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts index 9a42f58f3206e..4ab73ab2e8e64 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -5,33 +5,18 @@ * 2.0. */ -import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { PERFORM_RULE_UPGRADE_URL, PerformRuleUpgradeRequestBody, - ModeEnum, - PickVersionValuesEnum, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { PerformRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { buildSiemResponse } from '../../../routes/utils'; -import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors'; -import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; -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 { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; import { - PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, PREBUILT_RULES_OPERATION_CONCURRENCY, + PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, } from '../../constants'; -import { getUpgradeableRules } from './get_upgradeable_rules'; -import { createModifiedPrebuiltRuleAssets } from './create_upgradeable_rules_payload'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; -import { validatePerformRuleUpgradeRequest } from './validate_perform_rule_upgrade_request'; -import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; +import { performRuleUpgradeHandler } from './perform_rule_upgrade_handler'; export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -59,119 +44,6 @@ export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => }, }, }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - - try { - const ctx = await context.resolve(['core', 'alerting', 'securitySolution', 'licensing']); - const soClient = ctx.core.savedObjects.client; - const rulesClient = await ctx.alerting.getRulesClient(); - const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient(); - const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); - const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - - const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus(); - const defaultPickVersion = isRulesCustomizationEnabled - ? PickVersionValuesEnum.MERGED - : PickVersionValuesEnum.TARGET; - - validatePerformRuleUpgradeRequest({ - isRulesCustomizationEnabled, - payload: request.body, - defaultPickVersion, - }); - - const { mode, dry_run: isDryRun, on_conflict: onConflict } = request.body; - - const versionSpecifiers = mode === ModeEnum.ALL_RULES ? undefined : request.body.rules; - const filter = mode === ModeEnum.ALL_RULES ? request.body.filter : undefined; - const ruleTriadsMap = await fetchRuleVersionsTriad({ - ruleAssetsClient, - ruleObjectsClient, - versionSpecifiers, - filter, - }); - const ruleGroups = getRuleGroups(ruleTriadsMap); - - const ruleErrors = []; - - const { upgradeableRules, skippedRules, fetchErrors } = getUpgradeableRules({ - rawUpgradeableRules: ruleGroups.upgradeableRules, - currentRules: ruleGroups.currentRules, - versionSpecifiers, - mode, - onConflict, - }); - ruleErrors.push(...fetchErrors); - - const { modifiedPrebuiltRuleAssets, processingErrors } = createModifiedPrebuiltRuleAssets( - { - upgradeableRules, - requestBody: request.body, - defaultPickVersion, - } - ); - ruleErrors.push(...processingErrors); - - if (isDryRun) { - const body: PerformRuleUpgradeResponseBody = { - summary: { - total: upgradeableRules.length + skippedRules.length, - skipped: skippedRules.length, - succeeded: modifiedPrebuiltRuleAssets.length, - failed: ruleErrors.length, - }, - results: { - updated: modifiedPrebuiltRuleAssets.map((rule) => - convertPrebuiltRuleAssetToRuleResponse(rule) - ), - skipped: skippedRules, - }, - errors: aggregatePrebuiltRuleErrors(ruleErrors), - }; - return response.ok({ body }); - } - - const { results: updatedRules, errors: installationErrors } = await upgradePrebuiltRules( - detectionRulesClient, - modifiedPrebuiltRuleAssets - ); - ruleErrors.push(...installationErrors); - - const { error: timelineInstallationError } = await performTimelinesInstallation( - ctx.securitySolution - ); - - const allErrors = aggregatePrebuiltRuleErrors(ruleErrors); - if (timelineInstallationError) { - allErrors.push({ - message: timelineInstallationError, - rules: [], - }); - } - - const body: PerformRuleUpgradeResponseBody = { - summary: { - total: updatedRules.length + skippedRules.length + ruleErrors.length, - skipped: skippedRules.length, - succeeded: updatedRules.length, - failed: ruleErrors.length, - }, - results: { - updated: updatedRules.map(({ result }) => result), - skipped: skippedRules, - }, - errors: allErrors, - }; - - return response.ok({ body }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } + performRuleUpgradeHandler ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts index 4d35b98718a98..0bf1ee5cfc80a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts @@ -16,9 +16,7 @@ import { buildSiemResponse } from '../../../routes/utils'; import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; 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 { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; export const reviewRuleInstallationHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -34,15 +32,21 @@ export const reviewRuleInstallationHandler = async ( const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const ruleVersionsMap = await fetchRuleVersionsTriad({ - ruleAssetsClient, - ruleObjectsClient, + const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); + const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); + const currentRuleVersionsMap = new Map( + currentRuleVersions.map((version) => [version.rule_id, version]) + ); + + const installableRules = allLatestVersions.filter((latestVersion) => { + const currentVersion = currentRuleVersionsMap.get(latestVersion.rule_id); + return !currentVersion; }); - const { installableRules } = getRuleGroups(ruleVersionsMap); + const installableRuleAssets = await ruleAssetsClient.fetchAssetsByVersion(installableRules); const body: ReviewRuleInstallationResponseBody = { - stats: calculateRuleStats(installableRules), - rules: installableRules.map((prebuiltRuleAsset) => + stats: calculateRuleStats(installableRuleAssets), + rules: installableRuleAssets.map((prebuiltRuleAsset) => convertPrebuiltRuleAssetToRuleResponse(prebuiltRuleAsset) ), }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index b2b6367303d6c..42d0c75ef4df8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -106,6 +106,10 @@ export const createPrebuiltRuleObjectsClient = ( }, fetchInstalledRulesByIds: ({ ruleIds, sortField = 'createdAt', sortOrder = 'desc' }) => { return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRulesByIds', async () => { + if (ruleIds.length === 0) { + return []; + } + const { data } = await findRules({ rulesClient, perPage: ruleIds.length, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/preview_prebuilt_rules_upgrade.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/preview_prebuilt_rules_upgrade.ts index 9d80d14257c37..d5fbc330ee3e2 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/preview_prebuilt_rules_upgrade.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/preview_prebuilt_rules_upgrade.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../../../../../ftr_provider_context'; import { deleteAllPrebuiltRuleAssets, fetchFirstPrebuiltRuleUpgradeReviewDiff, - reviewPrebuiltRulesToUpgrade, } from '../../../../utils'; import { setUpRuleUpgrade } from '../../../../utils/rules/prebuilt_rules/set_up_rule_upgrade'; @@ -36,241 +35,6 @@ export default ({ getService }: FtrProviderContext): void => { describe( withHistoricalVersions ? 'with historical versions' : 'without historical versions', () => { - describe('stats', () => { - it('returns num of rules with upgrades', async () => { - await setUpRuleUpgrade({ - assets: [ - { - installed: { - rule_id: 'query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'query-rule', - type: 'query', - version: 2, - }, - }, - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - version: 2, - }, - }, - ], - removeInstalledAssets: !withHistoricalVersions, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - num_rules_to_upgrade_total: 2, - }); - }); - - it('returns zero conflicts when there are no conflicts', async () => { - await setUpRuleUpgrade({ - assets: [ - { - installed: { - rule_id: 'query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'query-rule', - type: 'query', - version: 2, - }, - }, - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - version: 2, - }, - }, - ], - removeInstalledAssets: !withHistoricalVersions, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - num_rules_with_conflicts: 0, - num_rules_with_non_solvable_conflicts: 0, - }); - }); - - it('returns num of rules with conflicts', async () => { - await setUpRuleUpgrade({ - assets: [ - { - installed: { - rule_id: 'query-rule', - type: 'query', - name: 'Initial name', - version: 1, - }, - patch: { - rule_id: 'query-rule', - name: 'Customized name', - }, - upgrade: { - rule_id: 'query-rule', - type: 'query', - name: 'Updated name', - version: 2, - }, - }, - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - tags: ['tagA'], - version: 1, - }, - patch: { - rule_id: 'saved-query-rule', - tags: ['tagB'], - }, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - tags: ['tagC'], - version: 2, - }, - }, - ], - removeInstalledAssets: !withHistoricalVersions, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - num_rules_with_conflicts: 2, - }); - }); - - it('returns num of rules with non-solvable conflicts', async () => { - await setUpRuleUpgrade({ - assets: [ - // Name field has a non-solvable upgrade conflict - { - installed: { - rule_id: 'query-rule', - type: 'query', - name: 'Initial name', - version: 1, - }, - patch: { - rule_id: 'query-rule', - name: 'Customized name', - }, - upgrade: { - rule_id: 'query-rule', - type: 'query', - name: 'Updated name', - version: 2, - }, - }, - // tags field values are merged resulting in a solvable upgrade conflict - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - tags: ['tagA'], - version: 1, - }, - patch: { - rule_id: 'saved-query-rule', - tags: ['tagB'], - }, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - tags: ['tagC'], - version: 2, - }, - }, - ], - removeInstalledAssets: !withHistoricalVersions, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - // Missing rule's base version doesn't allow to detect non solvable conflicts - num_rules_with_non_solvable_conflicts: withHistoricalVersions ? 1 : 0, - }); - }); - - if (!withHistoricalVersions) { - it('returns num of rules with conflicts caused by missing historical versions', async () => { - await setUpRuleUpgrade({ - assets: [ - { - installed: { - rule_id: 'query-rule', - type: 'query', - name: 'Initial name', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'query-rule', - type: 'query', - version: 2, - }, - }, - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - name: 'Updated name', - version: 2, - }, - }, - ], - removeInstalledAssets: true, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - num_rules_with_conflicts: 2, - }); - }); - } - }); - describe('fields diff stats', () => { it('returns num of fields with updates', async () => { await setUpRuleUpgrade({ From 18e700c9e45b8c75a703e8a7fa732841ac7c6d62 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Tue, 25 Feb 2025 22:45:48 -0500 Subject: [PATCH 06/18] refactors upgrade rules context --- .../pages/rule_details/index.tsx | 41 +- .../has_rule_update_callout.tsx | 45 ++ .../rule_update_callouts/translations.ts | 30 ++ .../upgrade_prebuilt_rules_table_context.tsx | 388 ++-------------- .../use_prebuilt_rules_upgrade.tsx | 424 ++++++++++++++++++ 5 files changed, 563 insertions(+), 365 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/has_rule_update_callout.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 5a5fa5e10795a..9e417bea4d083 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -148,6 +148,8 @@ import { useManualRuleRunConfirmation } from '../../../rule_gaps/components/manu import { useLegacyUrlRedirect } from './use_redirect_legacy_url'; import { RuleDetailTabs, useRuleDetailsTabs } from './use_rule_details_tabs'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { usePrebuiltRulesUpgrade } from '../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade'; +import { HasRuleUpdateCallout } from '../../../rule_management_ui/components/rule_update_callouts/has_rule_update_callout'; const RULE_EXCEPTION_LIST_TYPES = [ ExceptionListTypeEnum.DETECTION, @@ -252,9 +254,36 @@ const RuleDetailsPageComponent: React.FC = ({ isExistingRule, } = useRuleWithFallback(ruleId); + const onUpgrade = useCallback(() => { + refreshRule(); + }, [refreshRule]); + + const { + upgradeReviewResponse, + isLoading: isRuleUpgradeReviewLoading, + rulePreviewFlyout, + loadingRules, + openRulePreview, + } = usePrebuiltRulesUpgrade({ + pagination: { + page: 1, // we only want to fetch one result + perPage: 1, + }, + filter: { rule_ids: [ruleId] }, + onUpgrade, + }); + + const isRuleUpgradeable = useMemo( + () => upgradeReviewResponse !== undefined && upgradeReviewResponse.total > 0, + [upgradeReviewResponse] + ); + const { pollForSignalIndex } = useSignalHelpers(); const [rule, setRule] = useState(null); - const isLoading = ruleLoading && rule == null; + const isLoading = useMemo( + () => (ruleLoading && rule == null) || isRuleUpgradeReviewLoading || loadingRules.length > 0, + [isRuleUpgradeReviewLoading, loadingRules.length, rule, ruleLoading] + ); const { starting: isStartingJobs, startMlJobs } = useStartMlJobs(); const startMlJobsIfNeeded = useCallback(async () => { @@ -316,8 +345,8 @@ const RuleDetailsPageComponent: React.FC = ({ useLegacyUrlRedirect({ rule, spacesApi }); const showUpdating = useMemo( - () => isLoadingIndexPattern || isAlertsLoading || loading, - [isLoadingIndexPattern, isAlertsLoading, loading] + () => isLoadingIndexPattern || isAlertsLoading || loading || isRuleUpgradeReviewLoading, + [isLoadingIndexPattern, isAlertsLoading, loading, isRuleUpgradeReviewLoading] ); const title = useMemo( @@ -555,6 +584,11 @@ const RuleDetailsPageComponent: React.FC = ({ <> + {isBulkDuplicateConfirmationVisible && ( = ({ {isManualRuleRunConfirmationVisible && ( )} + {rulePreviewFlyout} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/has_rule_update_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/has_rule_update_callout.tsx new file mode 100644 index 0000000000000..5726da761b850 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/has_rule_update_callout.tsx @@ -0,0 +1,45 @@ +/* + * 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 { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import type { RuleResponse } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; + +interface HasRuleUpdateCalloutComponent { + rule: RuleResponse | null; + hasUpdate: boolean; + openRulePreview: (ruleId: string) => void; +} + +const HasRuleUpdateCalloutComponent = ({ + rule, + hasUpdate, + openRulePreview, +}: HasRuleUpdateCalloutComponent) => { + if (!rule || rule.rule_source.type !== 'external' || !hasUpdate) { + return null; + } + return ( + <> + +

{i18n.HAS_RULE_UPDATE_CALLOUT_MESSAGE}

+ { + openRulePreview(rule.rule_id); + }} + data-test-subj="ruleName" + > + {i18n.HAS_RULE_UPDATE_CALLOUT_BUTTON} + +
+ + + ); +}; + +export const HasRuleUpdateCallout = React.memo(HasRuleUpdateCalloutComponent); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts new file mode 100644 index 0000000000000..ac40d80694000 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts @@ -0,0 +1,30 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const HAS_RULE_UPDATE_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutTitle', + { + defaultMessage: 'Rule has available update', + } +); + +export const HAS_RULE_UPDATE_CALLOUT_MESSAGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutMessage', + { + defaultMessage: + 'This prebuilt rule has an update available, please upgrade to keep your rules up to date', + } +); + +export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutButton', + { + defaultMessage: 'Review rule for upgrade', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 7253c9fb8af02..54789e4c77b79 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -5,46 +5,20 @@ * 2.0. */ -import { EuiButton, EuiToolTip } from '@elastic/eui'; import type { Dispatch, SetStateAction } from 'react'; -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; +import React, { createContext, useContext, useMemo, useState } from 'react'; import type { FindRulesSortField, PrebuiltRuleFilter, - RuleFieldsToUpgrade, - RuleUpgradeSpecifier, SortOrder, } from '../../../../../../common/api/detection_engine'; -import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; -import { RuleUpgradeTab } from '../../../../rule_management/components/rule_details/three_way_diff'; -import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; -import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; -import type { - RuleResponse, - RuleSignatureId, -} from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { invariant } from '../../../../../../common/utils/invariant'; -import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout'; -import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; -import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; -import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; -import { FieldUpgradeStateEnum } from '../../../../rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum'; -import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; -import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; -import { useOutdatedMlJobsUpgradeModal } from './use_ml_jobs_upgrade_modal'; -import { useUpgradeWithConflictsModal } from './use_upgrade_with_conflicts_modal'; -import { RuleTypeChangeCallout } from './rule_type_change_callout'; -import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader'; -import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; -import * as i18n from './translations'; -import { CustomizationDisabledCallout } from './customization_disabled_callout'; import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../constants'; import type { PaginationOptions } from '../../../../rule_management/logic'; import { usePrebuiltRulesStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; - -const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; +import { usePrebuiltRulesUpgrade } from './use_prebuilt_rules_upgrade'; export interface UpgradePrebuiltRulesSortingOptions { field: @@ -111,8 +85,6 @@ export interface UpgradePrebuiltRulesTableState { sortingOptions: UpgradePrebuiltRulesSortingOptions; } -export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; - export interface UpgradePrebuiltRulesTableActions { reFetchRules: () => void; upgradeRules: (ruleIds: RuleSignatureId[]) => void; @@ -146,9 +118,6 @@ interface UpgradePrebuiltRulesTableContextProviderProps { export const UpgradePrebuiltRulesTableContextProvider = ({ children, }: UpgradePrebuiltRulesTableContextProviderProps) => { - const { isRulesCustomizationEnabled, customizationDisabledReason } = - usePrebuiltRulesCustomizationStatus(); - // Use the data from the prebuilt rules status API to determine if there are // rules to upgrade because it returns information about all rules without filters const { data: prebuiltRulesStatusResponse } = usePrebuiltRulesStatus(); @@ -156,9 +125,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ (prebuiltRulesStatusResponse?.stats.num_prebuilt_rules_to_upgrade ?? 0) > 0; const tags = prebuiltRulesStatusResponse?.aggregated_fields?.upgradeable_rules.tags; - const [loadingRules, setLoadingRules] = useState([]); const [filterOptions, setFilterOptions] = useState({}); - const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); const [pagination, setPagination] = useState({ page: 1, perPage: RULES_TABLE_INITIAL_PAGE_SIZE, @@ -182,319 +149,34 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ ); const { - data: upgradeReviewResponse, - refetch, - dataUpdatedAt, + ruleUpgradeStates, + upgradeReviewResponse, isFetched, isLoading, isFetching, isRefetching, - } = usePrebuiltRulesUpgradeReview( - { - page: pagination.page, - per_page: pagination.perPage, - sort: { - field: findRulesSortField, - order: sortingOptions.order, - }, - filter: filterOptions, - }, - { - refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL, - keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change - } - ); - - const upgradeableRules = useMemo( - () => upgradeReviewResponse?.rules ?? [], - [upgradeReviewResponse] - ); - - const { rulesUpgradeState, setRuleFieldResolvedValue } = - usePrebuiltRulesUpgradeState(upgradeableRules); - const ruleUpgradeStates = useMemo(() => Object.values(rulesUpgradeState), [rulesUpgradeState]); - - const { - modal: confirmLegacyMlJobsUpgradeModal, - confirmLegacyMLJobs, - isLoading: areMlJobsLoading, - } = useOutdatedMlJobsUpgradeModal(); - const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal(); - - const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules(); - - const upgradeRulesToResolved = useCallback( - async (ruleIds: RuleSignatureId[]) => { - const conflictRuleIdsSet = new Set( - ruleIds.filter( - (ruleId) => - rulesUpgradeState[ruleId].diff.num_fields_with_conflicts > 0 && - rulesUpgradeState[ruleId].hasUnresolvedConflicts - ) - ); - const upgradingRuleIds = ruleIds.filter((ruleId) => !conflictRuleIdsSet.has(ruleId)); - const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = upgradingRuleIds.map((ruleId) => ({ - rule_id: ruleId, - version: rulesUpgradeState[ruleId].target_rule.version, - revision: rulesUpgradeState[ruleId].revision, - fields: constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]), - })); - - setLoadingRules((prev) => [...prev, ...upgradingRuleIds]); - - try { - // Handle MLJobs modal - if (!(await confirmLegacyMLJobs())) { - return; - } - - if (conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade())) { - return; - } - - await upgradeRulesRequest({ - mode: 'SPECIFIC_RULES', - pick_version: 'MERGED', - rules: ruleUpgradeSpecifiers, - }); - } catch { - // Error is handled by the mutation's onError callback, so no need to do anything here - } finally { - const upgradedRuleIdsSet = new Set(upgradingRuleIds); - - setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); - } - }, - [confirmLegacyMLJobs, confirmConflictsUpgrade, rulesUpgradeState, upgradeRulesRequest] - ); - - const upgradeRulesToTarget = useCallback( - async (ruleIds: RuleSignatureId[]) => { - const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds.map((ruleId) => ({ - rule_id: ruleId, - version: rulesUpgradeState[ruleId].target_rule.version, - revision: rulesUpgradeState[ruleId].revision, - })); - - setLoadingRules((prev) => [...prev, ...ruleIds]); - - try { - // Handle MLJobs modal - if (!(await confirmLegacyMLJobs())) { - return; - } - - await upgradeRulesRequest({ - mode: 'SPECIFIC_RULES', - pick_version: 'TARGET', - rules: ruleUpgradeSpecifiers, - }); - } catch { - // Error is handled by the mutation's onError callback, so no need to do anything here - } finally { - const upgradedRuleIdsSet = new Set(ruleIds); - - setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); - } - }, - [confirmLegacyMLJobs, rulesUpgradeState, upgradeRulesRequest] - ); - - const upgradeRules = useCallback( - async (ruleIds: RuleSignatureId[]) => { - if (isRulesCustomizationEnabled) { - await upgradeRulesToResolved(ruleIds); - } else { - await upgradeRulesToTarget(ruleIds); - } - }, - [isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget] - ); - - const upgradeAllRules = useCallback(async () => { - setLoadingRules((prev) => [...prev, ...upgradeableRules.map((rule) => rule.rule_id)]); - - try { - // Handle MLJobs modal - if (!(await confirmLegacyMLJobs())) { - return; - } - - const dryRunResults = await upgradeRulesRequest({ - mode: 'ALL_RULES', - pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', - filter: filterOptions, - dry_run: true, - on_conflict: 'SKIP', - }); - - const hasConflicts = dryRunResults.results.skipped.some( - (skippedRule) => skippedRule.reason === 'CONFLICT' - ); - - if (hasConflicts && !(await confirmConflictsUpgrade())) { - return; - } - - await upgradeRulesRequest({ - mode: 'ALL_RULES', - pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', - filter: filterOptions, - on_conflict: 'SKIP', - }); - } catch { - // Error is handled by the mutation's onError callback, so no need to do anything here - } finally { - setLoadingRules([]); - } - }, [ - upgradeableRules, - confirmLegacyMLJobs, - upgradeRulesRequest, - isRulesCustomizationEnabled, - filterOptions, - confirmConflictsUpgrade, - ]); - - const subHeaderFactory = useCallback( - (rule: RuleResponse) => - rulesUpgradeState[rule.rule_id] ? ( - - ) : null, - [rulesUpgradeState] - ); - const ruleActionsFactory = useCallback( - (rule: RuleResponse, closeRulePreview: () => void, isEditingRule: boolean) => { - const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; - if (!ruleUpgradeState) { - return null; - } - - const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false; - return ( - { - if (hasRuleTypeChange || isRulesCustomizationEnabled === false) { - // If there is a rule type change, we can't resolve conflicts, only accept the target rule - upgradeRulesToTarget([rule.rule_id]); - } else { - upgradeRulesToResolved([rule.rule_id]); - } - closeRulePreview(); - }} - fill - data-test-subj="updatePrebuiltRuleFromFlyoutButton" - > - {i18n.UPDATE_BUTTON_LABEL} - - ); - }, - [ - rulesUpgradeState, - loadingRules, - isRefetching, - isUpgradingSecurityPackages, - isRulesCustomizationEnabled, - upgradeRulesToTarget, - upgradeRulesToResolved, - ] - ); - const extraTabsFactory = useCallback( - (rule: RuleResponse) => { - const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; - - if (!ruleUpgradeState) { - return []; - } - - const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false; - const hasCustomizations = - ruleUpgradeState.current_rule.rule_source.type === 'external' && - ruleUpgradeState.current_rule.rule_source.is_customized; - - let headerCallout = null; - if ( - hasCustomizations && - customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License - ) { - headerCallout = ; - } else if (hasRuleTypeChange && isRulesCustomizationEnabled) { - headerCallout = ; - } - - let updateTabContent = ( - - ); - - // Show the resolver tab only if rule customization is enabled and there - // is no rule type change. In case of rule type change users can't resolve - // conflicts, only accept the target rule. - if (isRulesCustomizationEnabled && !hasRuleTypeChange) { - updateTabContent = ( - - ); - } - - const updatesTab = { - id: 'updates', - name: ( - - <>{ruleDetailsI18n.UPDATES_TAB_LABEL} - - ), - content: {updateTabContent}, - }; - - const jsonViewTab = { - id: 'jsonViewUpdates', - name: ( - - <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} - - ), - content: ( -
- -
- ), - }; - - return [updatesTab, jsonViewTab]; - }, - [ - rulesUpgradeState, - customizationDisabledReason, - isRulesCustomizationEnabled, - setRuleFieldResolvedValue, - ] - ); - const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ - rules: ruleUpgradeStates.map(({ target_rule: targetRule }) => targetRule), - subHeaderFactory, - ruleActionsFactory, - extraTabsFactory, - flyoutProps: { - id: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, - dataTestSubj: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, + isUpgradingSecurityPackages, + loadingRules, + lastUpdated, + rulePreviewFlyout, + confirmLegacyMlJobsUpgradeModal, + upgradeConflictsModal, + openRulePreview, + reFetchRules, + upgradeRules, + upgradeAllRules, + } = usePrebuiltRulesUpgrade({ + pagination, + sort: { + field: findRulesSortField, + order: sortingOptions.order, }, + filter: filterOptions, }); const actions = useMemo( () => ({ - reFetchRules: refetch, + reFetchRules, upgradeRules, upgradeAllRules, setFilterOptions, @@ -502,7 +184,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setPagination, setSortingOptions, }), - [refetch, upgradeRules, upgradeAllRules, openRulePreview] + [reFetchRules, upgradeRules, upgradeAllRules, openRulePreview] ); const providerValue = useMemo( @@ -513,12 +195,12 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ filterOptions, tags: tags ?? [], isFetched, - isLoading: isLoading || areMlJobsLoading, + isLoading, isFetching, isRefetching, isUpgradingSecurityPackages, loadingRules, - lastUpdated: dataUpdatedAt, + lastUpdated, pagination: { ...pagination, total: upgradeReviewResponse?.total ?? 0, @@ -534,12 +216,11 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ tags, isFetched, isLoading, - areMlJobsLoading, isFetching, isRefetching, isUpgradingSecurityPackages, loadingRules, - dataUpdatedAt, + lastUpdated, pagination, upgradeReviewResponse?.total, sortingOptions, @@ -568,20 +249,3 @@ export const useUpgradePrebuiltRulesTableContext = (): UpgradePrebuiltRulesConte return rulesTableContext; }; - -function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade { - const ruleFieldsToUpgrade: Record = {}; - - for (const [fieldName, fieldUpgradeState] of Object.entries( - ruleUpgradeState.fieldsUpgradeState - )) { - if (fieldUpgradeState.state === FieldUpgradeStateEnum.Accepted) { - ruleFieldsToUpgrade[fieldName] = { - pick_version: 'RESOLVED', - resolved_value: fieldUpgradeState.resolvedValue, - }; - } - } - - return ruleFieldsToUpgrade; -} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx new file mode 100644 index 0000000000000..af3ad677f4e9d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx @@ -0,0 +1,424 @@ +/* + * 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 React, { useCallback, useMemo, useState } from 'react'; +import { EuiButton, EuiToolTip } from '@elastic/eui'; +import { + FieldUpgradeStateEnum, + type RuleUpgradeState, +} from '../../../../rule_management/model/prebuilt_rule_upgrade'; +import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; +import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; +import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; +import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status'; +import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; +import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; +import type { + FindRulesSortField, + PrebuiltRuleFilter, + RuleFieldsToUpgrade, + RuleResponse, + RuleSignatureId, + RuleUpgradeSpecifier, +} from '../../../../../../common/api/detection_engine'; +import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; +import { useOutdatedMlJobsUpgradeModal } from './use_ml_jobs_upgrade_modal'; +import { useUpgradeWithConflictsModal } from './use_upgrade_with_conflicts_modal'; +import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; +import * as i18n from './translations'; +import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader'; +import { CustomizationDisabledCallout } from './customization_disabled_callout'; +import { RuleUpgradeTab } from '../../../../rule_management/components/rule_details/three_way_diff'; +import { TabContentPadding } from '../../../../../siem_migrations/rules/components/rule_details_flyout'; +import { RuleTypeChangeCallout } from './rule_type_change_callout'; +import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; +import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; +import type { UpgradePrebuiltRulesSortingOptions } from './upgrade_prebuilt_rules_table_context'; + +const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; + +export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; + +export interface UsePrebuiltRulesUpgradeParams { + pagination: { + page: number; + perPage: number; + }; + sort?: { order: UpgradePrebuiltRulesSortingOptions['order']; field: FindRulesSortField }; + filter: PrebuiltRuleFilter; + onUpgrade?: () => void; +} + +export function usePrebuiltRulesUpgrade({ + pagination, + sort, + filter, + onUpgrade, +}: UsePrebuiltRulesUpgradeParams) { + const { isRulesCustomizationEnabled, customizationDisabledReason } = + usePrebuiltRulesCustomizationStatus(); + const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); + const [loadingRules, setLoadingRules] = useState([]); + + const { + data: upgradeReviewResponse, + refetch, + dataUpdatedAt, + isFetched, + isLoading, + isFetching, + isRefetching, + } = usePrebuiltRulesUpgradeReview( + { + page: pagination.page, + per_page: pagination.perPage, + sort, + filter, + }, + { + refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL, + keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change + } + ); + + const upgradeableRules = useMemo( + () => upgradeReviewResponse?.rules ?? [], + [upgradeReviewResponse] + ); + + const { rulesUpgradeState, setRuleFieldResolvedValue } = + usePrebuiltRulesUpgradeState(upgradeableRules); + const ruleUpgradeStates = useMemo(() => Object.values(rulesUpgradeState), [rulesUpgradeState]); + + const { + modal: confirmLegacyMlJobsUpgradeModal, + confirmLegacyMLJobs, + isLoading: areMlJobsLoading, + } = useOutdatedMlJobsUpgradeModal(); + const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal(); + + const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules(); + + const upgradeRulesToResolved = useCallback( + async (ruleIds: RuleSignatureId[]) => { + const conflictRuleIdsSet = new Set( + ruleIds.filter( + (ruleId) => + rulesUpgradeState[ruleId].diff.num_fields_with_conflicts > 0 && + rulesUpgradeState[ruleId].hasUnresolvedConflicts + ) + ); + const upgradingRuleIds = ruleIds.filter((ruleId) => !conflictRuleIdsSet.has(ruleId)); + const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = upgradingRuleIds.map((ruleId) => ({ + rule_id: ruleId, + version: rulesUpgradeState[ruleId].target_rule.version, + revision: rulesUpgradeState[ruleId].revision, + fields: constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]), + })); + + setLoadingRules((prev) => [...prev, ...upgradingRuleIds]); + + try { + // Handle MLJobs modal + if (!(await confirmLegacyMLJobs())) { + return; + } + + if (conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade())) { + return; + } + + await upgradeRulesRequest({ + mode: 'SPECIFIC_RULES', + pick_version: 'MERGED', + rules: ruleUpgradeSpecifiers, + }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here + } finally { + const upgradedRuleIdsSet = new Set(upgradingRuleIds); + + if (onUpgrade) { + onUpgrade(); + } + + setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); + } + }, + [ + rulesUpgradeState, + confirmLegacyMLJobs, + confirmConflictsUpgrade, + upgradeRulesRequest, + onUpgrade, + ] + ); + + const upgradeRulesToTarget = useCallback( + async (ruleIds: RuleSignatureId[]) => { + const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds.map((ruleId) => ({ + rule_id: ruleId, + version: rulesUpgradeState[ruleId].target_rule.version, + revision: rulesUpgradeState[ruleId].revision, + })); + + setLoadingRules((prev) => [...prev, ...ruleIds]); + + try { + // Handle MLJobs modal + if (!(await confirmLegacyMLJobs())) { + return; + } + + await upgradeRulesRequest({ + mode: 'SPECIFIC_RULES', + pick_version: 'TARGET', + rules: ruleUpgradeSpecifiers, + }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here + } finally { + const upgradedRuleIdsSet = new Set(ruleIds); + + if (onUpgrade) { + onUpgrade(); + } + + setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); + } + }, + [confirmLegacyMLJobs, onUpgrade, rulesUpgradeState, upgradeRulesRequest] + ); + + const upgradeRules = useCallback( + async (ruleIds: RuleSignatureId[]) => { + if (isRulesCustomizationEnabled) { + await upgradeRulesToResolved(ruleIds); + } else { + await upgradeRulesToTarget(ruleIds); + } + }, + [isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget] + ); + + const upgradeAllRules = useCallback(async () => { + setLoadingRules((prev) => [...prev, ...upgradeableRules.map((rule) => rule.rule_id)]); + + try { + // Handle MLJobs modal + if (!(await confirmLegacyMLJobs())) { + return; + } + + const dryRunResults = await upgradeRulesRequest({ + mode: 'ALL_RULES', + pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', + filter, + dry_run: true, + on_conflict: 'SKIP', + }); + + const hasConflicts = dryRunResults.results.skipped.some( + (skippedRule) => skippedRule.reason === 'CONFLICT' + ); + + if (hasConflicts && !(await confirmConflictsUpgrade())) { + return; + } + + await upgradeRulesRequest({ + mode: 'ALL_RULES', + pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', + filter, + on_conflict: 'SKIP', + }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here + } finally { + setLoadingRules([]); + } + }, [ + upgradeableRules, + confirmLegacyMLJobs, + upgradeRulesRequest, + isRulesCustomizationEnabled, + filter, + confirmConflictsUpgrade, + ]); + + const subHeaderFactory = useCallback( + (rule: RuleResponse) => + rulesUpgradeState[rule.rule_id] ? ( + + ) : null, + [rulesUpgradeState] + ); + const ruleActionsFactory = useCallback( + (rule: RuleResponse, closeRulePreview: () => void, isEditingRule: boolean) => { + const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; + if (!ruleUpgradeState) { + return null; + } + + const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false; + return ( + { + if (hasRuleTypeChange || isRulesCustomizationEnabled === false) { + // If there is a rule type change, we can't resolve conflicts, only accept the target rule + upgradeRulesToTarget([rule.rule_id]); + } else { + upgradeRulesToResolved([rule.rule_id]); + } + closeRulePreview(); + }} + fill + data-test-subj="updatePrebuiltRuleFromFlyoutButton" + > + {i18n.UPDATE_BUTTON_LABEL} + + ); + }, + [ + rulesUpgradeState, + loadingRules, + isRefetching, + isUpgradingSecurityPackages, + isRulesCustomizationEnabled, + upgradeRulesToTarget, + upgradeRulesToResolved, + ] + ); + const extraTabsFactory = useCallback( + (rule: RuleResponse) => { + const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; + + if (!ruleUpgradeState) { + return []; + } + + const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false; + const hasCustomizations = + ruleUpgradeState.current_rule.rule_source.type === 'external' && + ruleUpgradeState.current_rule.rule_source.is_customized; + + let headerCallout = null; + if ( + hasCustomizations && + customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License + ) { + headerCallout = ; + } else if (hasRuleTypeChange && isRulesCustomizationEnabled) { + headerCallout = ; + } + + let updateTabContent = ( + + ); + + // Show the resolver tab only if rule customization is enabled and there + // is no rule type change. In case of rule type change users can't resolve + // conflicts, only accept the target rule. + if (isRulesCustomizationEnabled && !hasRuleTypeChange) { + updateTabContent = ( + + ); + } + + const updatesTab = { + id: 'updates', + name: ( + + <>{ruleDetailsI18n.UPDATES_TAB_LABEL} + + ), + content: {updateTabContent}, + }; + + const jsonViewTab = { + id: 'jsonViewUpdates', + name: ( + + <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} + + ), + content: ( +
+ +
+ ), + }; + + return [updatesTab, jsonViewTab]; + }, + [ + rulesUpgradeState, + customizationDisabledReason, + isRulesCustomizationEnabled, + setRuleFieldResolvedValue, + ] + ); + const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ + rules: ruleUpgradeStates.map(({ target_rule: targetRule }) => targetRule), + subHeaderFactory, + ruleActionsFactory, + extraTabsFactory, + flyoutProps: { + id: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, + dataTestSubj: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, + }, + }); + + return { + ruleUpgradeStates, + upgradeReviewResponse, + isFetched, + isLoading: isLoading || areMlJobsLoading, + isFetching, + isRefetching, + isUpgradingSecurityPackages, + loadingRules, + lastUpdated: dataUpdatedAt, + rulePreviewFlyout, + confirmLegacyMlJobsUpgradeModal, + upgradeConflictsModal, + openRulePreview, + reFetchRules: refetch, + upgradeRules, + upgradeAllRules, + }; +} + +function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade { + const ruleFieldsToUpgrade: Record = {}; + + for (const [fieldName, fieldUpgradeState] of Object.entries( + ruleUpgradeState.fieldsUpgradeState + )) { + if (fieldUpgradeState.state === FieldUpgradeStateEnum.Accepted) { + ruleFieldsToUpgrade[fieldName] = { + pick_version: 'RESOLVED', + resolved_value: fieldUpgradeState.resolvedValue, + }; + } + } + + return ruleFieldsToUpgrade; +} From 1e11d6be2749d8b596978ac5259ce9e7ea46751d Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Wed, 26 Feb 2025 16:06:01 -0500 Subject: [PATCH 07/18] adds callout messages --- .../pages/rule_editing/index.tsx | 22 +++- .../pages/rule_details/index.tsx | 1 + .../rule_upgrade/rule_upgrade.tsx | 8 ++ .../rule_upgrade/rule_upgrade_callout.tsx | 117 +++++++++++------- .../rule_upgrade/translations.tsx | 8 ++ .../components/mini_callout/translations.tsx | 2 +- .../has_rule_update_callout.tsx | 26 ++-- .../rule_update_callouts.tsx | 53 ++++---- .../rule_update_callouts/translations.ts | 12 +- .../pages/rule_management/index.tsx | 2 + .../detection_engine/rules/translations.ts | 16 +++ 11 files changed, 176 insertions(+), 91 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index 6643ad2328168..43cf3036560ff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -75,6 +75,8 @@ import { usePrebuiltRulesCustomizationStatus } from '../../../rule_management/lo import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit'; import { usePrebuiltRuleCustomizationUpsellingMessage } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rule_customization_upselling_message'; +import { usePrebuiltRulesUpgrade } from '../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade'; +import { HasRuleUpdateCallout } from '../../../rule_management_ui/components/rule_update_callouts/has_rule_update_callout'; const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { const { addSuccess } = useAppToasts(); @@ -172,7 +174,20 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { newTermsFields: defineStepData.newTermsFields, }); - const loading = userInfoLoading || listsConfigLoading; + const { upgradeReviewResponse, isLoading: isRuleUpgradeReviewLoading } = usePrebuiltRulesUpgrade({ + pagination: { + page: 1, // we only want to fetch one result + perPage: 1, + }, + filter: { rule_ids: [ruleId] }, + }); + + const isRuleUpgradeable = useMemo( + () => upgradeReviewResponse !== undefined && upgradeReviewResponse.total > 0, + [upgradeReviewResponse] + ); + + const loading = userInfoLoading || listsConfigLoading || isRuleUpgradeReviewLoading; const { isSavedQueryLoading, savedQuery } = useGetSavedQuery({ savedQueryId: 'saved_id' in rule ? rule.saved_id : undefined, ruleType: rule?.type, @@ -550,6 +565,11 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { setIsRulePreviewVisible={setIsRulePreviewVisible} togglePanel={togglePanel} /> + {invalidSteps.length > 0 && ( = ({ rule={rule} hasUpdate={isRuleUpgradeable} openRulePreview={openRulePreview} + message={ruleI18n.HAS_RULE_UPDATE_DETAILS_CALLOUT_MESSAGE} /> {isBulkDuplicateConfirmationVisible && ( @@ -48,6 +49,7 @@ export const RuleUpgrade = memo(function RuleUpgrade({ {fieldNames.map((fieldName) => ( @@ -86,6 +88,12 @@ function calcNumOfNonSolvableConflicts(ruleUpgradeState: RuleUpgradeState): numb ).length; } +function calcHasBaseVersion(ruleUpgradeState: RuleUpgradeState): boolean { + return Object.values(ruleUpgradeState.diff.fields).some( + (field) => field.has_base_version === true + ); +} + /** * Defines fields sorting order by state. * Lower number corresponds to higher priority. diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade_callout.tsx index 10f432e236828..08a313eb93ccf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade_callout.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { ActionRequiredBadge } from '../badges/action_required'; import { ReviewRequiredBadge } from '../badges/review_required_badge'; import { ReadyForUpgradeBadge } from '../badges/ready_for_upgrade_badge'; @@ -15,74 +15,97 @@ import * as i18n from './translations'; interface RuleUpgradeCalloutProps { numOfSolvableConflicts: number; numOfNonSolvableConflicts: number; + hasBaseVersion: boolean; } export function RuleUpgradeCallout({ numOfSolvableConflicts, numOfNonSolvableConflicts, + hasBaseVersion, }: RuleUpgradeCalloutProps): JSX.Element { + let missingBaseVersionCallout: JSX.Element | null = null; + if (!hasBaseVersion) { + missingBaseVersionCallout = ( + <> + +

{i18n.RULE_BASE_VERSION_IS_MISSING_DESCRIPTION}

+
+ + + ); + } + if (numOfNonSolvableConflicts > 0) { return ( - - {i18n.UPGRADE_STATUS} -   - -   - {i18n.RULE_HAS_CONFLICTS(numOfNonSolvableConflicts + numOfSolvableConflicts)} - - } - color="danger" - size="s" - > - {i18n.RULE_HAS_HARD_CONFLICTS_DESCRIPTION} -
    -
  • {i18n.RULE_HAS_HARD_CONFLICTS_KEEP_YOUR_CHANGES}
  • -
  • {i18n.RULE_HAS_HARD_CONFLICTS_ACCEPT_ELASTIC_UPDATE}
  • -
  • {i18n.RULE_HAS_HARD_CONFLICTS_EDIT_FINAL_VERSION}
  • -
-
+ <> + {missingBaseVersionCallout} + + {i18n.UPGRADE_STATUS} +   + +   + {i18n.RULE_HAS_CONFLICTS(numOfNonSolvableConflicts + numOfSolvableConflicts)} + + } + color="danger" + size="s" + > + {i18n.RULE_HAS_HARD_CONFLICTS_DESCRIPTION} +
    +
  • {i18n.RULE_HAS_HARD_CONFLICTS_KEEP_YOUR_CHANGES}
  • +
  • {i18n.RULE_HAS_HARD_CONFLICTS_ACCEPT_ELASTIC_UPDATE}
  • +
  • {i18n.RULE_HAS_HARD_CONFLICTS_EDIT_FINAL_VERSION}
  • +
+
+ ); } if (numOfSolvableConflicts > 0) { return ( + <> + {missingBaseVersionCallout} + + {i18n.UPGRADE_STATUS} +   + +   + {i18n.RULE_HAS_CONFLICTS(numOfSolvableConflicts)} + + } + color="warning" + size="s" + > + {i18n.RULE_HAS_SOFT_CONFLICTS_DESCRIPTION} +
    +
  • {i18n.RULE_HAS_SOFT_CONFLICTS_ACCEPT_SUGGESTED_UPDATE}
  • +
  • {i18n.RULE_HAS_SOFT_CONFLICTS_EDIT_FINAL_VERSION}
  • +
+
+ + ); + } + + return ( + <> + {missingBaseVersionCallout} {i18n.UPGRADE_STATUS}   - -   - {i18n.RULE_HAS_CONFLICTS(numOfSolvableConflicts)} + } - color="warning" + color="success" size="s" > - {i18n.RULE_HAS_SOFT_CONFLICTS_DESCRIPTION} -
    -
  • {i18n.RULE_HAS_SOFT_CONFLICTS_ACCEPT_SUGGESTED_UPDATE}
  • -
  • {i18n.RULE_HAS_SOFT_CONFLICTS_EDIT_FINAL_VERSION}
  • -
+

{i18n.RULE_IS_READY_FOR_UPGRADE_DESCRIPTION}

- ); - } - - return ( - - {i18n.UPGRADE_STATUS} -   - - - } - color="success" - size="s" - > -

{i18n.RULE_IS_READY_FOR_UPGRADE_DESCRIPTION}

-
+ ); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx index 812ec8c808730..8f91884429c68 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx @@ -158,3 +158,11 @@ export const FIELD_MODIFIED_BADGE_DESCRIPTION = i18n.translate( 'This field value differs from the one provided in the original version of the rule.', } ); + +export const RULE_BASE_VERSION_IS_MISSING_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeFlyout.baseVersionMissingDescription', + { + defaultMessage: + 'The original version of this rule is stale and cannot be fetched, the upgrade experience will only include your version and the incoming elastic version. Please keep your rules as up to date as possible in the future to avoid this.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx index 4d6a26042696c..5bfcea4adeab6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx @@ -34,7 +34,7 @@ type OnClick = () => void; export const getUpdateRulesCalloutTitle = (onClick: OnClick) => ( void; + openRulePreview?: (ruleId: string) => void; + message: string; } const HasRuleUpdateCalloutComponent = ({ rule, hasUpdate, openRulePreview, + message, }: HasRuleUpdateCalloutComponent) => { if (!rule || rule.rule_source.type !== 'external' || !hasUpdate) { return null; } return ( <> - -

{i18n.HAS_RULE_UPDATE_CALLOUT_MESSAGE}

- { - openRulePreview(rule.rule_id); - }} - data-test-subj="ruleName" - > - {i18n.HAS_RULE_UPDATE_CALLOUT_BUTTON} - + +

{message}

+ {openRulePreview && ( + { + openRulePreview(rule.rule_id); + }} + data-test-subj="ruleName" + > + {i18n.HAS_RULE_UPDATE_CALLOUT_BUTTON} + + )}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx index 6ca59501dbd7a..51467fdd0ead7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import React, { useCallback } from 'react'; import { SecurityPageName } from '../../../../app/types'; import { useGetSecuritySolutionLinkProps } from '../../../../common/components/links'; @@ -18,15 +18,23 @@ import { } from '../mini_callout/translations'; import { AllRulesTabs } from '../rules_table/rules_table_toolbar'; -export const RuleUpdateCallouts = () => { +interface RuleUpdateCalloutsProps { + shouldShowNewRulesCallout?: boolean; + shouldShowUpdateRulesCallout?: boolean; +} + +export const RuleUpdateCallouts = ({ + shouldShowNewRulesCallout = false, + shouldShowUpdateRulesCallout = false, +}: RuleUpdateCalloutsProps) => { const { data: prebuiltRulesStatus } = usePrebuiltRulesStatus(); const rulesToInstallCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_install ?? 0; const rulesToUpgradeCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_upgrade ?? 0; // Check against rulesInstalledCount since we don't want to show banners if we're showing the empty prompt - const shouldDisplayNewRulesCallout = rulesToInstallCount > 0; - const shouldDisplayUpdateRulesCallout = rulesToUpgradeCount > 0; + const shouldDisplayNewRulesCallout = shouldShowNewRulesCallout && rulesToInstallCount > 0; + const shouldDisplayUpdateRulesCallout = shouldShowUpdateRulesCallout && rulesToUpgradeCount > 0; const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); const { href } = getSecuritySolutionLinkProps({ @@ -42,22 +50,25 @@ export const RuleUpdateCallouts = () => { }, [navigateToUrl, href]); return ( - - {shouldDisplayUpdateRulesCallout && ( - - )} - {shouldDisplayNewRulesCallout && ( - - )} - + <> + + {shouldDisplayUpdateRulesCallout && ( + + )} + {shouldDisplayNewRulesCallout && ( + + )} + + + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts index ac40d80694000..aaf82ee6b7254 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts @@ -10,21 +10,13 @@ import { i18n } from '@kbn/i18n'; export const HAS_RULE_UPDATE_CALLOUT_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutTitle', { - defaultMessage: 'Rule has available update', - } -); - -export const HAS_RULE_UPDATE_CALLOUT_MESSAGE = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutMessage', - { - defaultMessage: - 'This prebuilt rule has an update available, please upgrade to keep your rules up to date', + defaultMessage: 'Rule update available', } ); export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutButton', { - defaultMessage: 'Review rule for upgrade', + defaultMessage: 'Review rule for update', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index 17c5368a4b1c5..12cd645f2cee4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -35,6 +35,7 @@ import { AllRules } from '../../components/rules_table'; import { RulesTableContextProvider } from '../../components/rules_table/rules_table/rules_table_context'; import { useInvalidateFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview_query'; import { HeaderPage } from '../../../../common/components/header_page'; +import { RuleUpdateCallouts } from '../../components/rule_update_callouts/rule_update_callouts'; const RulesPageComponent: React.FC = () => { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); @@ -168,6 +169,7 @@ const RulesPageComponent: React.FC = () => {
+ Date: Fri, 28 Feb 2025 18:11:10 -0500 Subject: [PATCH 08/18] language changes --- .../pages/rule_editing/index.tsx | 11 +++++ .../pages/rule_details/index.tsx | 17 +++++++- .../rule_upgrade/translations.tsx | 2 +- .../components/mini_callout/translations.tsx | 2 +- .../has_rule_update_callout.tsx | 17 ++------ .../rule_update_callouts/translations.ts | 11 +---- ...e_upgrade_prebuilt_rules_table_columns.tsx | 42 ++++++++++++------- .../detection_engine/rules/translations.ts | 26 ++++++++++-- 8 files changed, 85 insertions(+), 43 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index 43cf3036560ff..7870160f6ef9c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -11,6 +11,7 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiResizableContainer, EuiSpacer, EuiTab, @@ -524,6 +525,15 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { [navigateToApp, ruleId] ); + const updateCallToActionButton = useMemo( + () => ( + + {ruleI18n.HAS_RULE_UPDATE_EDITING_CALLOUT_BUTTON} + + ), + [goToDetailsRule] + ); + if ( redirectToDetections( isSignalIndexExists, @@ -569,6 +579,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { rule={rule} hasUpdate={isRuleUpgradeable} message={ruleI18n.HAS_RULE_UPDATE_EDITING_CALLOUT_MESSAGE} + actionButton={updateCallToActionButton} /> {invalidSteps.length > 0 && ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 5465c55f73fa3..df0964040856e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -13,6 +13,7 @@ import { EuiConfirmModal, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiLoadingSpinner, EuiSpacer, EuiToolTip, @@ -423,6 +424,20 @@ const RuleDetailsPageComponent: React.FC = ({ const lastExecutionDate = lastExecution?.date ?? ''; const lastExecutionMessage = lastExecution?.message ?? ''; + const updateCallToActionButton = useMemo( + () => ( + { + openRulePreview(ruleRuleId); + }} + data-test-subj="ruleDetailsUpdateRuleCalloutButton" + > + {ruleI18n.HAS_RULE_UPDATE_CALLOUT_BUTTON} + + ), + [openRulePreview, ruleRuleId] + ); + const ruleStatusInfo = useMemo(() => { return ( <> @@ -587,7 +602,7 @@ const RuleDetailsPageComponent: React.FC = ({ {isBulkDuplicateConfirmationVisible && ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx index 8f91884429c68..1c42cb99836fe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx @@ -163,6 +163,6 @@ export const RULE_BASE_VERSION_IS_MISSING_DESCRIPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.upgradeFlyout.baseVersionMissingDescription', { defaultMessage: - 'The original version of this rule is stale and cannot be fetched, the upgrade experience will only include your version and the incoming elastic version. Please keep your rules as up to date as possible in the future to avoid this.', + "The original, unedited version of this Elastic rule couldn't be found. This sometimes happens when a rule hasn't been updated in a while. You can still update this rule, but will only have access to its current version and the incoming Elastic update. Updating Elastic rules more often can help you avoid this in the future. We encourage you to review this update carefully and ensure your changes are not accidentally overwritten.", } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx index 5bfcea4adeab6..ff55d01fd4b56 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx @@ -34,7 +34,7 @@ type OnClick = () => void; export const getUpdateRulesCalloutTitle = (onClick: OnClick) => ( void; + actionButton?: JSX.Element; message: string; } const HasRuleUpdateCalloutComponent = ({ rule, hasUpdate, - openRulePreview, + actionButton, message, }: HasRuleUpdateCalloutComponent) => { if (!rule || rule.rule_source.type !== 'external' || !hasUpdate) { @@ -30,16 +30,7 @@ const HasRuleUpdateCalloutComponent = ({ <>

{message}

- {openRulePreview && ( - { - openRulePreview(rule.rule_id); - }} - data-test-subj="ruleName" - > - {i18n.HAS_RULE_UPDATE_CALLOUT_BUTTON} - - )} + {actionButton}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts index aaf82ee6b7254..d47d86b185166 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts @@ -8,15 +8,8 @@ import { i18n } from '@kbn/i18n'; export const HAS_RULE_UPDATE_CALLOUT_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutTitle', + 'xpack.securitySolution.detectionEngine.ruleDetailsUpdate.calloutTitle', { - defaultMessage: 'Rule update available', - } -); - -export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutButton', - { - defaultMessage: 'Review rule for update', + defaultMessage: 'Elastic rule update available', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx index f7f0493b822ce..014f107687c12 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx @@ -144,6 +144,7 @@ const MODIFIED_COLUMN: TableColumn = { const createUpgradeButtonColumn = ( upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'], + openRulePreview: UpgradePrebuiltRulesTableActions['openRulePreview'], loadingRules: RuleSignatureId[], isDisabled: boolean, isPrebuiltRulesCustomizationEnabled: boolean @@ -154,7 +155,7 @@ const createUpgradeButtonColumn = ( const isRuleUpgrading = loadingRules.includes(ruleId); const isDisabledByConflicts = isPrebuiltRulesCustomizationEnabled && record.hasUnresolvedConflicts; - const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled || isDisabledByConflicts; + const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled; const spinner = ( ); - const tooltipContent = isDisabledByConflicts - ? i18n.UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS - : undefined; + if (isDisabledByConflicts) { + return ( + + openRulePreview(ruleId)} + data-test-subj={`reviewSinglePrebuiltRuleButton-${ruleId}`} + > + {isRuleUpgrading ? spinner : i18n.REVIEW_RULE_BUTTON} + + + ); + } return ( - - upgradeRules([ruleId])} - data-test-subj={`upgradeSinglePrebuiltRuleButton-${ruleId}`} - > - {isRuleUpgrading ? spinner : i18n.UPDATE_RULE_BUTTON} - - + upgradeRules([ruleId])} + data-test-subj={`upgradeSinglePrebuiltRuleButton-${ruleId}`} + > + {isRuleUpgrading ? spinner : i18n.UPDATE_RULE_BUTTON} + ); }, width: '10%', @@ -189,7 +199,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING); const { state: { loadingRules, isRefetching, isUpgradingSecurityPackages }, - actions: { upgradeRules }, + actions: { upgradeRules, openRulePreview }, } = useUpgradePrebuiltRulesTableContext(); const isDisabled = isRefetching || isUpgradingSecurityPackages; @@ -234,6 +244,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { ? [ createUpgradeButtonColumn( upgradeRules, + openRulePreview, loadingRules, isDisabled, isRulesCustomizationEnabled @@ -246,6 +257,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { showRelatedIntegrations, hasCRUDPermissions, upgradeRules, + openRulePreview, loadingRules, isDisabled, isRulesCustomizationEnabled, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 1810cf3c31332..36e779970614d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -1094,8 +1094,7 @@ export const CLEAR_RULES_TABLE_FILTERS = i18n.translate( export const HAS_RULE_UPDATE_DETAILS_CALLOUT_MESSAGE = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutMessage', { - defaultMessage: - 'This prebuilt rule has an update available, please update to get the latest improvements.', + defaultMessage: 'Review the update to see the latest improvements, then update your rule.', } ); @@ -1103,7 +1102,21 @@ export const HAS_RULE_UPDATE_EDITING_CALLOUT_MESSAGE = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutMessage', { defaultMessage: - 'This prebuilt rule has an update available, please update to the latest version before making any edits.', + 'Before editing this rule, we strongly recommend that you update it to ensure you get the latest improvements.', + } +); + +export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetailsUpdate.calloutButton', + { + defaultMessage: 'Review update', + } +); + +export const HAS_RULE_UPDATE_EDITING_CALLOUT_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleEditingUpdate.calloutButton', + { + defaultMessage: 'Return to details', } ); @@ -1449,6 +1462,13 @@ export const UPDATE_RULE_BUTTON = i18n.translate( } ); +export const REVIEW_RULE_BUTTON = i18n.translate( + 'xpack.securitySolution.addRules.reviewRuleButton', + { + defaultMessage: 'Review rule', + } +); + export const UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.upgradeRules.button.conflicts', { From 625290fb5600ba8b34684d48307e360a7550f952 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Mon, 3 Mar 2025 13:18:34 -0500 Subject: [PATCH 09/18] fixes merge --- .../use_prebuilt_rules_upgrade.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx index af3ad677f4e9d..9aede8baf7487 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx @@ -19,7 +19,7 @@ import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebui import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; import type { FindRulesSortField, - PrebuiltRuleFilter, + PrebuiltRulesFilter, RuleFieldsToUpgrade, RuleResponse, RuleSignatureId, @@ -49,7 +49,7 @@ export interface UsePrebuiltRulesUpgradeParams { perPage: number; }; sort?: { order: UpgradePrebuiltRulesSortingOptions['order']; field: FindRulesSortField }; - filter: PrebuiltRuleFilter; + filter: PrebuiltRulesFilter; onUpgrade?: () => void; } From 9befc7968ff78563bb607d439cd614cc9a05476f Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Mon, 3 Mar 2025 15:07:41 -0500 Subject: [PATCH 10/18] removes unrelated code --- .../common/prebuilt_rule_filter.ts | 33 ------------------- .../review_rule_upgrade_route.ts | 5 +-- .../rule_upgrade/rule_upgrade.tsx | 9 +---- .../rule_upgrade_state.ts | 4 +++ .../use_prebuilt_rules_upgrade.tsx | 5 +-- .../use_prebuilt_rules_upgrade_state.ts | 6 ++++ .../calculate_rule_upgrade_info.ts | 2 ++ 7 files changed, 19 insertions(+), 45 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rule_filter.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rule_filter.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rule_filter.ts deleted file mode 100644 index 255af69c89cda..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rule_filter.ts +++ /dev/null @@ -1,33 +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 { z } from '@kbn/zod'; - -export enum RuleCustomizationStatus { - CUSTOMIZED = 'CUSTOMIZED', - NOT_CUSTOMIZED = 'NOT_CUSTOMIZED', -} - -export type PrebuiltRuleFilter = z.infer; -export const PrebuiltRuleFilter = z.object({ - /** - * Rule IDs to return upgrade info for - */ - rule_ids: z.array(z.string()).optional(), - /** - * Tags to filter by - */ - tags: z.array(z.string()).optional(), - /** - * Rule name to filter by - */ - name: z.string().optional(), - /** - * Rule customization status to filter by - */ - customization_status: z.nativeEnum(RuleCustomizationStatus).optional(), -}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts index 3fd95412b9311..014cb02ba50d8 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts @@ -10,7 +10,7 @@ import { SortOrder, type RuleObjectId, type RuleSignatureId, type RuleTagArray } import type { PartialRuleDiff } from '../model'; import type { RuleResponse, RuleVersion } from '../../model/rule_schema'; import { FindRulesSortField } from '../../rule_management'; -import { PrebuiltRulesFilter } from '../common/prebuilt_rules_filter'; +import { ReviewPrebuiltRuleUpgradeFilter } from '../common/review_prebuilt_rules_upgrade_filter'; export type ReviewRuleUpgradeSort = z.infer; export const ReviewRuleUpgradeSort = z.object({ @@ -27,7 +27,7 @@ export const ReviewRuleUpgradeSort = z.object({ export type ReviewRuleUpgradeRequestBody = z.infer; export const ReviewRuleUpgradeRequestBody = z .object({ - filter: PrebuiltRulesFilter.optional(), + filter: ReviewPrebuiltRuleUpgradeFilter.optional(), sort: ReviewRuleUpgradeSort.optional(), page: z.coerce.number().int().min(1).optional().default(1), @@ -89,4 +89,5 @@ export interface RuleUpgradeInfoForReview { target_rule: RuleResponse; diff: PartialRuleDiff; revision: number; + hasBaseVersion: boolean; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx index b0ab159c6aacf..c67dc53523279 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx @@ -33,7 +33,6 @@ export const RuleUpgrade = memo(function RuleUpgrade({ const numOfSolvableConflicts = calcNumOfSolvableConflicts(ruleUpgradeState); const numOfNonSolvableConflicts = calcNumOfNonSolvableConflicts(ruleUpgradeState); const fieldNames = extractSortedFieldNames(ruleUpgradeState.fieldsUpgradeState); - const hasBaseVersion = calcHasBaseVersion(ruleUpgradeState); return ( <> @@ -49,7 +48,7 @@ export const RuleUpgrade = memo(function RuleUpgrade({ {fieldNames.map((fieldName) => ( @@ -88,12 +87,6 @@ function calcNumOfNonSolvableConflicts(ruleUpgradeState: RuleUpgradeState): numb ).length; } -function calcHasBaseVersion(ruleUpgradeState: RuleUpgradeState): boolean { - return Object.values(ruleUpgradeState.diff.fields).some( - (field) => field.has_base_version === true - ); -} - /** * Defines fields sorting order by state. * Lower number corresponds to higher priority. diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts index 7cd1f9c9ef843..45428fa1bb387 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts @@ -17,4 +17,8 @@ export interface RuleUpgradeState extends RuleUpgradeInfoForReview { * Indicates whether there are conflicts blocking rule upgrading. */ hasUnresolvedConflicts: boolean; + /** + * Indicates whether there are non-solvable conflicts blocking rule upgrading. + */ + hasNonSolvableUnresolvedConflicts: boolean; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx index 9aede8baf7487..23aa750a0bed6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; +import type { ReviewPrebuiltRuleUpgradeFilter } from '../../../../../../common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter'; import { FieldUpgradeStateEnum, type RuleUpgradeState, @@ -19,7 +20,6 @@ import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebui import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; import type { FindRulesSortField, - PrebuiltRulesFilter, RuleFieldsToUpgrade, RuleResponse, RuleSignatureId, @@ -49,7 +49,7 @@ export interface UsePrebuiltRulesUpgradeParams { perPage: number; }; sort?: { order: UpgradePrebuiltRulesSortingOptions['order']; field: FindRulesSortField }; - filter: PrebuiltRulesFilter; + filter: ReviewPrebuiltRuleUpgradeFilter; onUpgrade?: () => void; } @@ -112,6 +112,7 @@ export function usePrebuiltRulesUpgrade({ rulesUpgradeState[ruleId].hasUnresolvedConflicts ) ); + const upgradingRuleIds = ruleIds.filter((ruleId) => !conflictRuleIdsSet.has(ruleId)); const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = upgradingRuleIds.map((ruleId) => ({ rule_id: ruleId, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts index 706bda1eb0d66..2a20068ab8639 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts @@ -134,6 +134,9 @@ export function usePrebuiltRulesUpgradeState( fieldState === FieldUpgradeStateEnum.SolvableConflict || fieldState === FieldUpgradeStateEnum.NonSolvableConflict ); + const hasNonSolvableFieldConflicts = Object.values(fieldsUpgradeState).some( + ({ state: fieldState }) => fieldState === FieldUpgradeStateEnum.NonSolvableConflict + ); state[ruleUpgradeInfo.rule_id] = { ...ruleUpgradeInfo, @@ -141,6 +144,9 @@ export function usePrebuiltRulesUpgradeState( hasUnresolvedConflicts: isRulesCustomizationEnabled ? hasRuleTypeChange || hasFieldConflicts : false, + hasNonSolvableUnresolvedConflicts: isRulesCustomizationEnabled + ? hasRuleTypeChange || hasNonSolvableFieldConflicts + : false, }; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts index 1bfd9f09c43f7..1fc6d54e00323 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts @@ -23,6 +23,7 @@ export const calculateRuleUpgradeInfo = ( const { ruleDiff, ruleVersions } = result; const installedCurrentVersion = ruleVersions.input.current; const targetVersion = ruleVersions.input.target; + const baseVersion = ruleVersions.input.base; invariant(installedCurrentVersion != null, 'installedCurrentVersion not found'); invariant(targetVersion != null, 'targetVersion not found'); @@ -43,6 +44,7 @@ export const calculateRuleUpgradeInfo = ( version: installedCurrentVersion.version, current_rule: installedCurrentVersion, target_rule: targetRule, + hasBaseVersion: baseVersion !== undefined, diff: { fields: pickBy>( ruleDiff.fields, From 0aed247442c7a5cdadc59a9ee26c6a579b1b7b0a Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Mon, 3 Mar 2025 15:49:54 -0500 Subject: [PATCH 11/18] updates unit tests --- .../data_source_diff_algorithm.test.ts | 189 ++++++--- .../eql_query_diff_algorithm.test.ts | 174 +++++--- .../esql_query_diff_algorithm.test.ts | 160 +++++--- .../kql_query_diff_algorithm.test.ts | 370 ++++++++++++------ .../algorithms/number_diff_algorithm.test.ts | 132 +++++-- .../rule_type_diff_algorithm.test.ts | 135 ++++--- .../single_line_string_diff_algorithm.test.ts | 132 +++++-- 7 files changed, 879 insertions(+), 413 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts index 6020738bd5e66..237c1a5651220 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts @@ -36,7 +36,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -55,7 +55,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '123' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -82,7 +82,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -110,7 +110,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -135,7 +135,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -159,7 +159,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -178,7 +178,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '456' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -208,7 +208,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -227,7 +227,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '456' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -262,7 +262,7 @@ describe('dataSourceDiffAlgorithm', () => { index_patterns: ['one', 'four', 'five'], }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -281,7 +281,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '789' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -311,7 +311,7 @@ describe('dataSourceDiffAlgorithm', () => { index_patterns: ['one', 'three', 'four', 'two', 'five'], }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -338,7 +338,7 @@ describe('dataSourceDiffAlgorithm', () => { data_view_id: '123', }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -360,7 +360,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '789' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -385,7 +385,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '789' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -407,7 +407,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -421,81 +421,162 @@ describe('dataSourceDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: DataSourceType.index_patterns, - index_patterns: ['one', 'three', 'four'], - }, - target_version: { - type: DataSourceType.index_patterns, - index_patterns: ['one', 'three', 'four'], - }, - }; - - const result = dataSourceDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); - }); - - describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - it('if versions are different types', () => { + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { const mockVersions: ThreeVersionsOf = { base_version: MissingVersion, - current_version: { type: DataSourceType.data_view, data_view_id: '456' }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, target_version: { type: DataSourceType.index_patterns, index_patterns: ['one', 'three', 'four'], }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ has_base_version: false, base_version: undefined, merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, + conflict: ThreeWayDiffConflict.NONE, }) ); }); - it('if current version is undefined', () => { + it('returns NONE conflict if rule is customized', () => { const mockVersions: ThreeVersionsOf = { base_version: MissingVersion, - current_version: undefined, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, target_version: { type: DataSourceType.index_patterns, index_patterns: ['one', 'three', 'four'], }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, true); expect(result).toEqual( expect.objectContaining({ has_base_version: false, base_version: undefined, merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, + conflict: ThreeWayDiffConflict.NONE, }) ); }); }); + + describe('if current_version and target_version are different - scenario -AB', () => { + describe('returns NONE conflict if rule is NOT customized', () => { + it('if versions are different types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { type: DataSourceType.data_view, data_view_id: '456' }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('if current version is undefined', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: undefined, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + }); + + describe('returns SOLVABLE conflict if rule is customized', () => { + it('if versions are different types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { type: DataSourceType.data_view, data_view_id: '456' }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); + + it('if current version is undefined', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: undefined, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); + }); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.test.ts index acbb63016aeda..1b51b8a7e47ed 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.test.ts @@ -37,7 +37,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -68,7 +68,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -99,7 +99,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -130,7 +130,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -162,7 +162,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -193,7 +193,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -207,62 +207,124 @@ describe('eqlQueryDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - query: 'query where true', - language: 'eql', - filters: [], - }, - target_version: { - query: 'query where true', - language: 'eql', - filters: [], - }, - }; + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + }; + + const result = eqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - query: 'query where true', - language: 'eql', - filters: [], - }, - target_version: { - query: 'query where false', - language: 'eql', - filters: [], - }, - }; + describe('if current_version and target_version are different - scenario -AB', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + }; + + const result = eqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.test.ts index ecaaa4142b5e0..96ab96d36a670 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.test.ts @@ -34,7 +34,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -62,7 +62,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -90,7 +90,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -118,7 +118,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -147,7 +147,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -161,58 +161,116 @@ describe('esqlQueryDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - query: 'query where true', - language: 'esql', - }, - target_version: { - query: 'query where true', - language: 'esql', - }, - }; + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'esql', + }, + target_version: { + query: 'query where true', + language: 'esql', + }, + }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'esql', + }, + target_version: { + query: 'query where true', + language: 'esql', + }, + }; + + const result = esqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - query: 'query where true', - language: 'esql', - }, - target_version: { - query: 'query where false', - language: 'esql', - }, - }; + describe('if current_version and target_version are different - scenario -AB', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'esql', + }, + target_version: { + query: 'query where false', + language: 'esql', + }, + }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'esql', + }, + target_version: { + query: 'query where false', + language: 'esql', + }, + }; + + const result = esqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.test.ts index fe97a222448df..b6acdf7450fce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.test.ts @@ -43,7 +43,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -71,7 +71,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -103,7 +103,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -137,7 +137,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -171,7 +171,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -205,7 +205,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -237,7 +237,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -271,7 +271,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -302,7 +302,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -338,7 +338,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -372,7 +372,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -406,7 +406,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -422,124 +422,250 @@ describe('kqlQueryDiffAlgorithm', () => { describe('if base_version is missing', () => { describe('if current_version and target_version are the same - scenario -AA', () => { - it('returns current_version as merged output if all versions are inline query types', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: KqlQueryType.inline_query, - query: 'query string = true', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - target_version: { - type: KqlQueryType.inline_query, - query: 'query string = true', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - }; - - const result = kqlQueryDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if rule is NOT customized', () => { + it('returns current_version as merged output if all versions are inline query types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns current_version as merged output if all versions are saved query types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + target_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns current_version as merged output if all versions are saved query types', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: KqlQueryType.saved_query, - saved_query_id: 'saved-query-id', - }, - target_version: { - type: KqlQueryType.saved_query, - saved_query_id: 'saved-query-id', - }, - }; - - const result = kqlQueryDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if rule is customized', () => { + it('returns current_version as merged output if all versions are inline query types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns current_version as merged output if all versions are saved query types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + target_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); }); describe('if current_version and target_version are different - scenario -AB', () => { - it('returns target_version as merged output if current and target versions have the same types', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: KqlQueryType.inline_query, - query: 'query string = true', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - target_version: { - type: KqlQueryType.inline_query, - query: 'query string = false', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - }; - - const result = kqlQueryDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('if rule is NOT customized', () => { + it('returns NONE conflict if current and target versions have the same types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if current and target versions have different types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id-2', + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current and target versions have different types', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: KqlQueryType.saved_query, - saved_query_id: 'saved-query-id-2', - }, - target_version: { - type: KqlQueryType.inline_query, - query: 'query string = false', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - }; - - const result = kqlQueryDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('if rule is customized', () => { + it('returns SOLVABLE conflict if current and target versions have the same types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); + + it('returns SOLVABLE conflict if current and target versions have different types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id-2', + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.test.ts index 837fb85c3b2c3..d1fa31605244e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.test.ts @@ -22,7 +22,7 @@ describe('numberDiffAlgorithm', () => { target_version: 1, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -41,7 +41,7 @@ describe('numberDiffAlgorithm', () => { target_version: 1, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -60,7 +60,7 @@ describe('numberDiffAlgorithm', () => { target_version: 2, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -79,7 +79,7 @@ describe('numberDiffAlgorithm', () => { target_version: 2, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -98,7 +98,7 @@ describe('numberDiffAlgorithm', () => { target_version: 3, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -111,46 +111,92 @@ describe('numberDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 1, - target_version: 1, - }; - - const result = numberDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 1, + target_version: 1, + }; + + const result = numberDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 1, + target_version: 1, + }; + + const result = numberDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 1, - target_version: 2, - }; - - const result = numberDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('if current_version and target_version are different - scenario -AB', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 1, + target_version: 2, + }; + + const result = numberDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 1, + target_version: 2, + }; + + const result = numberDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts index accf133ac71b3..8a51c3506d85a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts @@ -25,7 +25,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'query', }; - const result = ruleTypeDiffAlgorithm(mockVersions); + const result = ruleTypeDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -45,7 +45,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'query', }; - const result = ruleTypeDiffAlgorithm(mockVersions); + const result = ruleTypeDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -65,7 +65,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'saved_query', }; - const result = ruleTypeDiffAlgorithm(mockVersions); + const result = ruleTypeDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -85,7 +85,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'saved_query', }; - const result = ruleTypeDiffAlgorithm(mockVersions); + const result = ruleTypeDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -106,7 +106,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'saved_query', }; - const result = ruleTypeDiffAlgorithm(mockVersions); + const result = ruleTypeDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -119,47 +119,94 @@ describe('ruleTypeDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'query', - target_version: 'query', - }; - - const result = ruleTypeDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'query', + target_version: 'query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'query', + target_version: 'query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - // User can change rule type field between `query` and `saved_query` in the UI, no other rule types - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'query', - target_version: 'saved_query', - }; - - const result = ruleTypeDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.NON_SOLVABLE, - }) - ); + describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { + it('returns NON_SOLVABLE conflict if rule is NOT customized', () => { + // User can change rule type field between `query` and `saved_query` in the UI, no other rule types + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'query', + target_version: 'saved_query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }) + ); + }); + + it('returns NON_SOLVABLE conflict if rule is customized', () => { + // User can change rule type field between `query` and `saved_query` in the UI, no other rule types + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'query', + target_version: 'saved_query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.test.ts index af238283ca21f..cbeea00203e2e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.test.ts @@ -22,7 +22,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'A', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -41,7 +41,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'A', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -60,7 +60,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'B', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -79,7 +79,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'B', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -98,7 +98,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'C', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -111,46 +111,92 @@ describe('singleLineStringDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'A', - target_version: 'A', - }; - - const result = singleLineStringDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'A', + target_version: 'A', + }; + + const result = singleLineStringDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'A', + target_version: 'A', + }; + + const result = singleLineStringDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'A', - target_version: 'B', - }; - - const result = singleLineStringDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'A', + target_version: 'B', + }; + + const result = singleLineStringDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'A', + target_version: 'B', + }; + + const result = singleLineStringDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); From 30f99cc7b11445faeb6cfa2e0dc238e96d6c2388 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Mon, 3 Mar 2025 17:00:07 -0500 Subject: [PATCH 12/18] fixes types --- .../components/rules_table/feature_tour/rules_feature_tour.tsx | 2 +- .../use_prebuilt_rules_upgrade_state.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx index 96e05f490147c..fca637989f14b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx @@ -26,8 +26,8 @@ import React, { useEffect, useMemo } from 'react'; import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../../common/constants'; import { useKibana } from '../../../../../common/lib/kibana'; import { useIsElementMounted } from '../rules_table/guided_onboarding/use_is_element_mounted'; -import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from '../upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context'; import * as i18n from './translations'; +import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from '../upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade'; export interface RulesFeatureTourContextType { steps: EuiTourStepProps[]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts index 3a35e783975e9..7c3c54437e94b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts @@ -429,6 +429,7 @@ function createRuleUpgradeInfoMock( num_fields_with_non_solvable_conflicts: 0, fields: {}, }, + hasBaseVersion: true, version: 1, revision: 1, ...rewrites, From 4b7160cdcb7aae7efb40936e65308e66157889f4 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Mon, 3 Mar 2025 21:48:35 -0500 Subject: [PATCH 13/18] fixes tests --- .../common_fields/tags.ts | 3 ++- .../diffable_rule_fields/test_helpers.ts | 7 +++++- .../type_specific_fields/threat_index.ts | 3 ++- .../update_workflow_customized_rules.cy.ts | 25 ++++++++++--------- .../cypress/screens/alerts_detection_rules.ts | 4 +++ 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/tags.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/tags.ts index 880ec3ed29f3a..ecaa093a7f359 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/tags.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/tags.ts @@ -297,10 +297,11 @@ export function tagsField({ getService }: FtrProviderContext): void { ruleUpgradeAssets, diffableRuleFieldName: 'tags', expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + isMergableField: true, expectedFieldDiffValues: { current: ['tagB'], target: ['tagC'], - merged: ['tagC'], + merged: ['tagB', 'tagC'], }, }, getService diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/test_helpers.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/test_helpers.ts index 48302fa6ef8a3..e59308c7b0b2b 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/test_helpers.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/test_helpers.ts @@ -78,6 +78,7 @@ type ExpectedDiffOutcome = | { expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate; expectedFieldDiffValues: MissingHistoricalRuleVersionsFieldDiffValueVersions; + isMergableField?: boolean; }; /** @@ -160,6 +161,7 @@ export function testFieldUpgradeReview( expectMissingBaseABFieldDiff(diff, { diffableRuleFieldName: params.diffableRuleFieldName, valueVersions: params.expectedFieldDiffValues, + isMergableField: params.isMergableField, }); break; } @@ -495,6 +497,7 @@ function expectMissingBaseAAFieldDiff( interface MissingBaseFieldAssertParams { diffableRuleFieldName: string; valueVersions: MissingHistoricalRuleVersionsFieldDiffValueVersions; + isMergableField?: boolean; } /** @@ -518,7 +521,9 @@ function expectMissingBaseABFieldDiff( target_version: fieldAssertParams.valueVersions.target, merged_version: fieldAssertParams.valueVersions.merged, diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, + merge_outcome: fieldAssertParams.isMergableField + ? ThreeWayMergeOutcome.Merged + : ThreeWayMergeOutcome.Target, conflict: ThreeWayDiffConflict.SOLVABLE, }, isUndefined diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/threat_index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/threat_index.ts index 41c6c6f7cf2fd..6f04b950861bb 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/threat_index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/threat_index.ts @@ -302,10 +302,11 @@ export function threatIndexField({ getService }: FtrProviderContext): void { ruleUpgradeAssets, diffableRuleFieldName: 'threat_index', expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + isMergableField: true, expectedFieldDiffValues: { current: ['indexD'], target: ['indexB', 'indexC'], - merged: ['indexB', 'indexC'], + merged: ['indexD', 'indexB', 'indexC'], }, }, getService diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts index 515ec4e633127..e011a983384fc 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts @@ -16,6 +16,7 @@ import { RULES_UPDATES_TABLE, UPGRADE_ALL_RULES_BUTTON, UPGRADE_SELECTED_RULES_BUTTON, + getReviewSingleRuleButtonByRuleId, getUpgradeSingleRuleButtonByRuleId, } from '../../../../screens/alerts_detection_rules'; import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; @@ -102,11 +103,11 @@ describe( clickRuleUpdatesTab(); }); - it('should disable individual upgrade buttons for all prebuilt rules with conflicts', () => { - // All buttons should be disabled because of conflicts + it('should display individual review buttons for all prebuilt rules with conflicts', () => { + // All buttons should be review buttons because of conflicts for (const rule of [OUTDATED_RULE_1, OUTDATED_RULE_2]) { const { rule_id: ruleId } = rule['security-rule']; - expect(cy.get(getUpgradeSingleRuleButtonByRuleId(ruleId)).should('be.disabled')); + expect(cy.get(getReviewSingleRuleButtonByRuleId(ruleId)).should('exist')); } }); @@ -203,24 +204,24 @@ describe( assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_3]); }); - it('should disable the upgrade button for conflicting rules while allowing upgrades of no-conflict rules', () => { - // Verify the conflicting rule's upgrade button is disabled + it('should switch to a review button for conflicting rules while allowing upgrades of no-conflict rules', () => { + // Verify the conflicting rule's upgrade button has the review label expect( cy - .get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_1['security-rule'].rule_id)) - .should('be.disabled') + .get(getReviewSingleRuleButtonByRuleId(OUTDATED_RULE_1['security-rule'].rule_id)) + .should('exist') ); - // Verify non-conflicting rules' upgrade buttons are enabled + // Verify non-conflicting rules' upgrade buttons do not have the review label expect( cy .get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_2['security-rule'].rule_id)) - .should('not.be.disabled') + .should('exist') ); expect( cy .get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_3['security-rule'].rule_id)) - .should('not.be.disabled') + .should('exist') ); }); @@ -305,10 +306,10 @@ describe( }); it('should disable individual upgrade button for all rules', () => { - // All buttons should be disabled because rule type changes are considered conflicts + // All buttons should be displayed as review buttons because rule type changes are considered conflicts for (const rule of [OUTDATED_QUERY_RULE_1, OUTDATED_QUERY_RULE_2]) { const { rule_id: ruleId } = rule['security-rule']; - expect(cy.get(getUpgradeSingleRuleButtonByRuleId(ruleId)).should('be.disabled')); + expect(cy.get(getReviewSingleRuleButtonByRuleId(ruleId)).should('exist')); } }); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts index 012de17a07b4f..cf9a9d84737ee 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts @@ -196,6 +196,10 @@ export const getUpgradeSingleRuleButtonByRuleId = (ruleId: string) => { return `[data-test-subj="upgradeSinglePrebuiltRuleButton-${ruleId}"]`; }; +export const getReviewSingleRuleButtonByRuleId = (ruleId: string) => { + return `[data-test-subj="reviewSinglePrebuiltRuleButton-${ruleId}"]`; +}; + export const NO_RULES_AVAILABLE_FOR_INSTALL_MESSAGE = '[data-test-subj="noPrebuiltRulesAvailableForInstall"]'; export const NO_RULES_AVAILABLE_FOR_UPGRADE_MESSAGE = From f00f0624ba6f92c6625d79aab372028190ee12a5 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Mon, 3 Mar 2025 23:13:18 -0500 Subject: [PATCH 14/18] fixes tests --- .../diffable_rule_fields/common_fields/references.ts | 3 ++- .../type_specific_fields/new_terms_fields.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/references.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/references.ts index cd778ad7a4e40..fc50342ac1786 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/references.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/references.ts @@ -297,10 +297,11 @@ export function referencesField({ getService }: FtrProviderContext): void { ruleUpgradeAssets, diffableRuleFieldName: 'references', expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + isMergableField: true, expectedFieldDiffValues: { current: ['http://url-3'], target: ['http://url-1', 'http://url-2'], - merged: ['http://url-1', 'http://url-2'], + merged: ['http://url-3', 'http://url-1', 'http://url-2'], }, }, getService diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/new_terms_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/new_terms_fields.ts index 3af660439c1ee..14eb9b4fda39c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/new_terms_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/new_terms_fields.ts @@ -302,10 +302,11 @@ export function newTermsFieldsField({ getService }: FtrProviderContext): void { ruleUpgradeAssets, diffableRuleFieldName: 'new_terms_fields', expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + isMergableField: true, expectedFieldDiffValues: { current: ['fieldB'], target: ['fieldA', 'fieldC'], - merged: ['fieldA', 'fieldC'], + merged: ['fieldB', 'fieldA', 'fieldC'], }, }, getService From b58a1e2de9dc06ee650f8385a0a7430309a2c874 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Tue, 4 Mar 2025 12:49:32 -0500 Subject: [PATCH 15/18] addresses blocking comments --- .../review_rule_upgrade_route.ts | 2 +- ...tch_prebuilt_rules_install_review_query.ts | 2 + .../rule_upgrade/rule_upgrade.tsx | 2 +- .../use_prebuilt_rules_upgrade_state.test.ts | 2 +- .../calculate_rule_upgrade_info.ts | 2 +- .../multi_line_string_diff_algorithm.ts | 5 +- .../rule_type_diff_algorithm.test.ts | 61 +++---------------- .../algorithms/rule_type_diff_algorithm.ts | 21 ++----- 8 files changed, 23 insertions(+), 74 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts index 014cb02ba50d8..40c92733851cb 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts @@ -89,5 +89,5 @@ export interface RuleUpgradeInfoForReview { target_rule: RuleResponse; diff: PartialRuleDiff; revision: number; - hasBaseVersion: boolean; + has_base_version: boolean; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts index 0f7f5b512b0fa..adf28044f20ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts @@ -11,6 +11,7 @@ import { reviewRuleInstall } from '../../api'; import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; import type { ReviewRuleInstallationResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; +import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; export const REVIEW_RULE_INSTALLATION_QUERY_KEY = ['POST', REVIEW_RULE_INSTALLATION_URL]; @@ -26,6 +27,7 @@ export const useFetchPrebuiltRulesInstallReviewQuery = ( { ...DEFAULT_QUERY_OPTIONS, ...options, + retry: retryOnRateLimitedError, } ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx index c67dc53523279..8adef539f9987 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx @@ -48,7 +48,7 @@ export const RuleUpgrade = memo(function RuleUpgrade({ {fieldNames.map((fieldName) => ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts index 7c3c54437e94b..f83aa914432a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts @@ -429,7 +429,7 @@ function createRuleUpgradeInfoMock( num_fields_with_non_solvable_conflicts: 0, fields: {}, }, - hasBaseVersion: true, + has_base_version: true, version: 1, revision: 1, ...rewrites, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts index 1fc6d54e00323..0649c8aaa9490 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts @@ -44,7 +44,7 @@ export const calculateRuleUpgradeInfo = ( version: installedCurrentVersion.version, current_rule: installedCurrentVersion, target_rule: targetRule, - hasBaseVersion: baseVersion !== undefined, + has_base_version: baseVersion !== undefined, diff: { fields: pickBy>( ruleDiff.fields, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts index 2b788ad36704a..4cfe5b782fb44 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts @@ -132,8 +132,9 @@ const mergeVersions = ({ // Missing base versions always return target version // If the rule is customized, we return a SOLVABLE conflict - // Otherwise we treat scenario -AB as AAB - // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + // Since multi-line string fields are mergeable, we would typically return a merged value + // as per https://github.com/elastic/kibana/pull/211862, but with no base version we cannot + // complete a full diff merge and so just return the target version case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts index 8a51c3506d85a..4c77d61536005 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts @@ -25,7 +25,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'query', }; - const result = ruleTypeDiffAlgorithm(mockVersions, false); + const result = ruleTypeDiffAlgorithm(mockVersions); expect(result).toEqual( expect.objectContaining({ @@ -45,7 +45,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'query', }; - const result = ruleTypeDiffAlgorithm(mockVersions, false); + const result = ruleTypeDiffAlgorithm(mockVersions); expect(result).toEqual( expect.objectContaining({ @@ -65,7 +65,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'saved_query', }; - const result = ruleTypeDiffAlgorithm(mockVersions, false); + const result = ruleTypeDiffAlgorithm(mockVersions); expect(result).toEqual( expect.objectContaining({ @@ -85,7 +85,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'saved_query', }; - const result = ruleTypeDiffAlgorithm(mockVersions, false); + const result = ruleTypeDiffAlgorithm(mockVersions); expect(result).toEqual( expect.objectContaining({ @@ -106,7 +106,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'saved_query', }; - const result = ruleTypeDiffAlgorithm(mockVersions, false); + const result = ruleTypeDiffAlgorithm(mockVersions); expect(result).toEqual( expect.objectContaining({ @@ -120,35 +120,14 @@ describe('ruleTypeDiffAlgorithm', () => { describe('if base_version is missing', () => { describe('if current_version and target_version are the same - scenario -AA', () => { - it('returns NONE conflict if rule is NOT customized', () => { + it('returns NONE conflict', () => { const mockVersions: ThreeVersionsOf = { base_version: MissingVersion, current_version: 'query', target_version: 'query', }; - const result = ruleTypeDiffAlgorithm(mockVersions, false); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.NONE, - }) - ); - }); - - it('returns NONE conflict if rule is customized', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'query', - target_version: 'query', - }; - - const result = ruleTypeDiffAlgorithm(mockVersions, true); + const result = ruleTypeDiffAlgorithm(mockVersions); expect(result).toEqual( expect.objectContaining({ @@ -164,29 +143,7 @@ describe('ruleTypeDiffAlgorithm', () => { }); describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - it('returns NON_SOLVABLE conflict if rule is NOT customized', () => { - // User can change rule type field between `query` and `saved_query` in the UI, no other rule types - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'query', - target_version: 'saved_query', - }; - - const result = ruleTypeDiffAlgorithm(mockVersions, false); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.NON_SOLVABLE, - }) - ); - }); - - it('returns NON_SOLVABLE conflict if rule is customized', () => { + it('returns NON_SOLVABLE conflict', () => { // User can change rule type field between `query` and `saved_query` in the UI, no other rule types const mockVersions: ThreeVersionsOf = { base_version: MissingVersion, @@ -194,7 +151,7 @@ describe('ruleTypeDiffAlgorithm', () => { target_version: 'saved_query', }; - const result = ruleTypeDiffAlgorithm(mockVersions, true); + const result = ruleTypeDiffAlgorithm(mockVersions); expect(result).toEqual( expect.objectContaining({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts index f27a99a7b43e5..efb115feb7419 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts @@ -21,8 +21,7 @@ import { } from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; export const ruleTypeDiffAlgorithm = ( - versions: ThreeVersionsOf, - isRuleCustomized: boolean + versions: ThreeVersionsOf ): ThreeWayDiff => { const { base_version: baseVersion, @@ -38,7 +37,6 @@ export const ruleTypeDiffAlgorithm = ( const { mergeOutcome, conflict, mergedVersion } = mergeVersions({ targetVersion, diffOutcome, - isRuleCustomized, }); return { @@ -64,15 +62,17 @@ interface MergeResult { interface MergeArgs { targetVersion: TValue; diffOutcome: ThreeWayDiffOutcome; - isRuleCustomized: boolean; } const mergeVersions = ({ targetVersion, diffOutcome, - isRuleCustomized, }: MergeArgs): MergeResult => { switch (diffOutcome) { + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: return { conflict: ThreeWayDiffConflict.NONE, @@ -94,17 +94,6 @@ const mergeVersions = ({ conflict: ThreeWayDiffConflict.NON_SOLVABLE, }; } - - // Missing base versions always return target version - // Scenario -AA is treated as AAA - // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: { - return { - mergedVersion: targetVersion, - mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.NONE, - }; - } default: return assertUnreachable(diffOutcome); } From 1e76dfc5d2ce29b357060d1cc9071a5b0897e1de Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Tue, 4 Mar 2025 12:58:58 -0500 Subject: [PATCH 16/18] adds back merge omition --- .../use_fetch_prebuilt_rules_install_review_query.ts | 2 ++ .../use_fetch_prebuilt_rules_upgrade_review_query.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts index adf28044f20ce..9d88e8984962f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts @@ -12,6 +12,7 @@ import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/api/detec import type { ReviewRuleInstallationResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; +import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const REVIEW_RULE_INSTALLATION_QUERY_KEY = ['POST', REVIEW_RULE_INSTALLATION_URL]; @@ -28,6 +29,7 @@ export const useFetchPrebuiltRulesInstallReviewQuery = ( ...DEFAULT_QUERY_OPTIONS, ...options, retry: retryOnRateLimitedError, + retryDelay: cappedExponentialBackoff, } ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts index 3ff49ab1b9161..4b779918febc3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts @@ -14,6 +14,8 @@ import type { ReviewRuleUpgradeResponseBody, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; +import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; +import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const REVIEW_RULE_UPGRADE_QUERY_KEY = ['POST', REVIEW_RULE_UPGRADE_URL]; @@ -30,6 +32,8 @@ export const useFetchPrebuiltRulesUpgradeReviewQuery = ( { ...DEFAULT_QUERY_OPTIONS, ...options, + retry: retryOnRateLimitedError, + retryDelay: cappedExponentialBackoff, } ); }; From 8133b1ab5510b30032265960fc5c01cc83b2bd97 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Tue, 4 Mar 2025 17:27:20 -0500 Subject: [PATCH 17/18] addresses comments --- .../detection_engine/rule_management/utils.ts | 5 +- .../pages/rule_editing/index.tsx | 34 +++------ .../pages/rule_details/index.tsx | 63 +++------------- .../rule_details/rule_update_callout.tsx | 71 +++++++++++++++++++ .../components/rule_details/translations.tsx | 14 ++++ .../hooks}/use_prebuilt_rules_upgrade.tsx | 47 ++++++------ .../hooks/use_rule_update_callout.tsx | 32 +++++++++ .../has_rule_update_callout.tsx | 40 ----------- .../rule_update_callouts/translations.ts | 15 ---- .../feature_tour/rules_feature_tour.tsx | 2 +- .../upgrade_prebuilt_rules_table_context.tsx | 2 +- .../detection_engine/rules/translations.ts | 7 -- .../logic/diff/calculate_rule_diff.ts | 6 +- 13 files changed, 166 insertions(+), 172 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_update_callout.tsx rename x-pack/solutions/security/plugins/security_solution/public/detection_engine/{rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table => rule_management/hooks}/use_prebuilt_rules_upgrade.tsx (83%) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_rule_update_callout.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/has_rule_update_callout.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/utils.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/utils.ts index c8e32c471c2c5..84f095463ef74 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/utils.ts @@ -6,7 +6,7 @@ */ import { ecsFieldMap } from '@kbn/alerts-as-data-utils'; -import type { RequiredField, RequiredFieldInput } from '../../api/detection_engine'; +import type { RequiredField, RequiredFieldInput, RuleResponse } from '../../api/detection_engine'; /* Computes the boolean "ecs" property value for each required field based on the ECS field map. @@ -23,3 +23,6 @@ export const addEcsToRequiredFields = (requiredFields?: RequiredFieldInput[]): R ecs: isEcsField, }; }); + +export const isRuleCustomized = (rule: RuleResponse) => + rule.rule_source.type === 'external' && rule.rule_source.is_customized === true; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index 7870160f6ef9c..e64d51dad9d95 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -76,8 +76,7 @@ import { usePrebuiltRulesCustomizationStatus } from '../../../rule_management/lo import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit'; import { usePrebuiltRuleCustomizationUpsellingMessage } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rule_customization_upselling_message'; -import { usePrebuiltRulesUpgrade } from '../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade'; -import { HasRuleUpdateCallout } from '../../../rule_management_ui/components/rule_update_callouts/has_rule_update_callout'; +import { useRuleUpdateCallout } from '../../../rule_management/hooks/use_rule_update_callout'; const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { const { addSuccess } = useAppToasts(); @@ -175,20 +174,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { newTermsFields: defineStepData.newTermsFields, }); - const { upgradeReviewResponse, isLoading: isRuleUpgradeReviewLoading } = usePrebuiltRulesUpgrade({ - pagination: { - page: 1, // we only want to fetch one result - perPage: 1, - }, - filter: { rule_ids: [ruleId] }, - }); - - const isRuleUpgradeable = useMemo( - () => upgradeReviewResponse !== undefined && upgradeReviewResponse.total > 0, - [upgradeReviewResponse] - ); - - const loading = userInfoLoading || listsConfigLoading || isRuleUpgradeReviewLoading; + const loading = userInfoLoading || listsConfigLoading; const { isSavedQueryLoading, savedQuery } = useGetSavedQuery({ savedQueryId: 'saved_id' in rule ? rule.saved_id : undefined, ruleType: rule?.type, @@ -525,14 +511,15 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { [navigateToApp, ruleId] ); - const updateCallToActionButton = useMemo( - () => ( + const upgradeCallout = useRuleUpdateCallout({ + rule, + message: ruleI18n.HAS_RULE_UPDATE_EDITING_CALLOUT_MESSAGE, + actionButton: ( {ruleI18n.HAS_RULE_UPDATE_EDITING_CALLOUT_BUTTON} ), - [goToDetailsRule] - ); + }); if ( redirectToDetections( @@ -575,12 +562,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { setIsRulePreviewVisible={setIsRulePreviewVisible} togglePanel={togglePanel} /> - + {upgradeCallout} {invalidSteps.length > 0 && ( = ({ isExistingRule, } = useRuleWithFallback(ruleId); - const onUpgrade = useCallback(() => { - refreshRule(); - }, [refreshRule]); - - const { - upgradeReviewResponse, - isLoading: isRuleUpgradeReviewLoading, - rulePreviewFlyout, - loadingRules, - openRulePreview, - } = usePrebuiltRulesUpgrade({ - pagination: { - page: 1, // we only want to fetch one result - perPage: 1, - }, - filter: { rule_ids: [ruleId] }, - onUpgrade, - }); - - const isRuleUpgradeable = useMemo( - () => upgradeReviewResponse !== undefined && upgradeReviewResponse.total > 0, - [upgradeReviewResponse] - ); - const { pollForSignalIndex } = useSignalHelpers(); const [rule, setRule] = useState(null); - const isLoading = useMemo( - () => (ruleLoading && rule == null) || isRuleUpgradeReviewLoading || loadingRules.length > 0, - [isRuleUpgradeReviewLoading, loadingRules.length, rule, ruleLoading] - ); + const isLoading = useMemo(() => ruleLoading && rule == null, [rule, ruleLoading]); const { starting: isStartingJobs, startMlJobs } = useStartMlJobs(); const startMlJobsIfNeeded = useCallback(async () => { @@ -346,8 +317,8 @@ const RuleDetailsPageComponent: React.FC = ({ useLegacyUrlRedirect({ rule, spacesApi }); const showUpdating = useMemo( - () => isLoadingIndexPattern || isAlertsLoading || loading || isRuleUpgradeReviewLoading, - [isLoadingIndexPattern, isAlertsLoading, loading, isRuleUpgradeReviewLoading] + () => isLoadingIndexPattern || isAlertsLoading || loading, + [isLoadingIndexPattern, isAlertsLoading, loading] ); const title = useMemo( @@ -424,19 +395,11 @@ const RuleDetailsPageComponent: React.FC = ({ const lastExecutionDate = lastExecution?.date ?? ''; const lastExecutionMessage = lastExecution?.message ?? ''; - const updateCallToActionButton = useMemo( - () => ( - { - openRulePreview(ruleRuleId); - }} - data-test-subj="ruleDetailsUpdateRuleCalloutButton" - > - {ruleI18n.HAS_RULE_UPDATE_CALLOUT_BUTTON} - - ), - [openRulePreview, ruleRuleId] - ); + const upgradeCallout = useRuleUpdateCallout({ + rule, + message: ruleI18n.HAS_RULE_UPDATE_DETAILS_CALLOUT_MESSAGE, + onUpgrade: refreshRule, + }); const ruleStatusInfo = useMemo(() => { return ( @@ -599,12 +562,7 @@ const RuleDetailsPageComponent: React.FC = ({ <> - + {upgradeCallout} {isBulkDuplicateConfirmationVisible && ( = ({ {isManualRuleRunConfirmationVisible && ( )} - {rulePreviewFlyout} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_update_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_update_callout.tsx new file mode 100644 index 0000000000000..991ae099770f4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_update_callout.tsx @@ -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 { EuiCallOut, EuiSpacer, EuiLink } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import type { RuleResponse } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; +import { usePrebuiltRulesUpgrade } from '../../hooks/use_prebuilt_rules_upgrade'; + +interface RuleUpdateCalloutProps { + rule: RuleResponse; + message: string; + actionButton?: JSX.Element; + onUpgrade?: () => void; +} + +const RuleUpdateCalloutComponent = ({ + rule, + message, + actionButton, + onUpgrade, +}: RuleUpdateCalloutProps): JSX.Element | null => { + const { upgradeReviewResponse, rulePreviewFlyout, openRulePreview } = usePrebuiltRulesUpgrade({ + pagination: { + page: 1, // we only want to fetch one result + perPage: 1, + }, + filter: { rule_ids: [rule.id] }, + onUpgrade, + }); + + const isRuleUpgradeable = useMemo( + () => upgradeReviewResponse !== undefined && upgradeReviewResponse.total > 0, + [upgradeReviewResponse] + ); + + const updateCallToActionButton = useMemo(() => { + if (actionButton) { + return actionButton; + } + return ( + openRulePreview(rule.rule_id)} + data-test-subj="ruleDetailsUpdateRuleCalloutButton" + > + {i18n.HAS_RULE_UPDATE_CALLOUT_BUTTON} + + ); + }, [actionButton, openRulePreview, rule.rule_id]); + + if (!isRuleUpgradeable) { + return null; + } + + return ( + <> + +

{message}

+ {updateCallToActionButton} +
+ + {rulePreviewFlyout} + + ); +}; + +export const RuleUpdateCallout = React.memo(RuleUpdateCalloutComponent); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx index b72ec31ac63b3..04efe7de06c43 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx @@ -461,3 +461,17 @@ export const LUCENE_LANGUAGE_LABEL = i18n.translate( defaultMessage: 'Lucene', } ); + +export const HAS_RULE_UPDATE_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.updateCalloutTitle', + { + defaultMessage: 'Elastic rule update available', + } +); + +export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetailsUpdate.calloutButton', + { + defaultMessage: 'Review update', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx similarity index 83% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx index 23aa750a0bed6..053351836518c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx @@ -7,37 +7,34 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; -import type { ReviewPrebuiltRuleUpgradeFilter } from '../../../../../../common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter'; -import { - FieldUpgradeStateEnum, - type RuleUpgradeState, -} from '../../../../rule_management/model/prebuilt_rule_upgrade'; -import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; -import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; -import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; -import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status'; -import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; -import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; +import type { ReviewPrebuiltRuleUpgradeFilter } from '../../../../common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter'; +import { FieldUpgradeStateEnum, type RuleUpgradeState } from '../model/prebuilt_rule_upgrade'; +import { PerFieldRuleDiffTab } from '../components/rule_details/per_field_rule_diff_tab'; +import { PrebuiltRulesCustomizationDisabledReason } from '../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; +import { useIsUpgradingSecurityPackages } from '../logic/use_upgrade_security_packages'; +import { usePrebuiltRulesCustomizationStatus } from '../logic/prebuilt_rules/use_prebuilt_rules_customization_status'; +import { usePerformUpgradeRules } from '../logic/prebuilt_rules/use_perform_rule_upgrade'; +import { usePrebuiltRulesUpgradeReview } from '../logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; import type { FindRulesSortField, RuleFieldsToUpgrade, RuleResponse, RuleSignatureId, RuleUpgradeSpecifier, -} from '../../../../../../common/api/detection_engine'; -import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; -import { useOutdatedMlJobsUpgradeModal } from './use_ml_jobs_upgrade_modal'; -import { useUpgradeWithConflictsModal } from './use_upgrade_with_conflicts_modal'; -import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; -import * as i18n from './translations'; -import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader'; -import { CustomizationDisabledCallout } from './customization_disabled_callout'; -import { RuleUpgradeTab } from '../../../../rule_management/components/rule_details/three_way_diff'; -import { TabContentPadding } from '../../../../../siem_migrations/rules/components/rule_details_flyout'; -import { RuleTypeChangeCallout } from './rule_type_change_callout'; -import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; -import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; -import type { UpgradePrebuiltRulesSortingOptions } from './upgrade_prebuilt_rules_table_context'; +} from '../../../../common/api/detection_engine'; +import { usePrebuiltRulesUpgradeState } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; +import { useOutdatedMlJobsUpgradeModal } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_ml_jobs_upgrade_modal'; +import { useUpgradeWithConflictsModal } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal'; +import * as ruleDetailsI18n from '../components/rule_details/translations'; +import * as i18n from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations'; +import { UpgradeFlyoutSubHeader } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_flyout_subheader'; +import { CustomizationDisabledCallout } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/customization_disabled_callout'; +import { RuleUpgradeTab } from '../components/rule_details/three_way_diff'; +import { TabContentPadding } from '../../../siem_migrations/rules/components/rule_details_flyout'; +import { RuleTypeChangeCallout } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/rule_type_change_callout'; +import { RuleDiffTab } from '../components/rule_details/rule_diff_tab'; +import { useRulePreviewFlyout } from '../../rule_management_ui/components/rules_table/use_rule_preview_flyout'; +import type { UpgradePrebuiltRulesSortingOptions } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context'; const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_rule_update_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_rule_update_callout.tsx new file mode 100644 index 0000000000000..a4bff69d4fb99 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_rule_update_callout.tsx @@ -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 React from 'react'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import { RuleUpdateCallout } from '../components/rule_details/rule_update_callout'; + +interface UseRuleUpdateCalloutProps { + rule: RuleResponse | null; + message: string; + actionButton?: JSX.Element; + onUpgrade?: () => void; +} + +export const useRuleUpdateCallout = ({ + rule, + message, + actionButton, + onUpgrade, +}: UseRuleUpdateCalloutProps): JSX.Element | null => + !rule || rule.rule_source.type !== 'external' ? null : ( + + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/has_rule_update_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/has_rule_update_callout.tsx deleted file mode 100644 index ba3b17f1bbcb4..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/has_rule_update_callout.tsx +++ /dev/null @@ -1,40 +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 { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React from 'react'; -import type { RuleResponse } from '../../../../../common/api/detection_engine'; -import * as i18n from './translations'; - -interface HasRuleUpdateCalloutComponent { - rule: RuleResponse | null; - hasUpdate: boolean; - actionButton?: JSX.Element; - message: string; -} - -const HasRuleUpdateCalloutComponent = ({ - rule, - hasUpdate, - actionButton, - message, -}: HasRuleUpdateCalloutComponent) => { - if (!rule || rule.rule_source.type !== 'external' || !hasUpdate) { - return null; - } - return ( - <> - -

{message}

- {actionButton} -
- - - ); -}; - -export const HasRuleUpdateCallout = React.memo(HasRuleUpdateCalloutComponent); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts deleted file mode 100644 index d47d86b185166..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/translations.ts +++ /dev/null @@ -1,15 +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 { i18n } from '@kbn/i18n'; - -export const HAS_RULE_UPDATE_CALLOUT_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetailsUpdate.calloutTitle', - { - defaultMessage: 'Elastic rule update available', - } -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx index fca637989f14b..28d798ff06a99 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx @@ -27,7 +27,7 @@ import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../../common/constan import { useKibana } from '../../../../../common/lib/kibana'; import { useIsElementMounted } from '../rules_table/guided_onboarding/use_is_element_mounted'; import * as i18n from './translations'; -import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from '../upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade'; +import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from '../../../../rule_management/hooks/use_prebuilt_rules_upgrade'; export interface RulesFeatureTourContextType { steps: EuiTourStepProps[]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 07bf025ac7cb2..c59f4ec4be57b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -18,7 +18,7 @@ import { invariant } from '../../../../../../common/utils/invariant'; import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../constants'; import type { PaginationOptions } from '../../../../rule_management/logic'; import { usePrebuiltRulesStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; -import { usePrebuiltRulesUpgrade } from './use_prebuilt_rules_upgrade'; +import { usePrebuiltRulesUpgrade } from '../../../../rule_management/hooks/use_prebuilt_rules_upgrade'; export interface UpgradePrebuiltRulesSortingOptions { field: diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 36e779970614d..33896a138aaa2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -1106,13 +1106,6 @@ export const HAS_RULE_UPDATE_EDITING_CALLOUT_MESSAGE = i18n.translate( } ); -export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetailsUpdate.calloutButton', - { - defaultMessage: 'Review update', - } -); - export const HAS_RULE_UPDATE_EDITING_CALLOUT_BUTTON = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleEditingUpdate.calloutButton', { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts index 5fb266f06427c..a5fd84f4b097a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isRuleCustomized } from '../../../../../../common/detection_engine/rule_management/utils'; import type { DiffableRule, FullRuleDiff, @@ -67,8 +68,7 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult = invariant(current != null, 'current version is required'); const diffableCurrentVersion = convertRuleToDiffable(current); - const isRuleCustomized = - current.rule_source.type === 'external' && current.rule_source.is_customized === true; + const isCustomized = isRuleCustomized(current); invariant(target != null, 'target version is required'); const diffableTargetVersion = convertRuleToDiffable( @@ -86,7 +86,7 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult = current_version: diffableCurrentVersion, target_version: diffableTargetVersion, }, - isRuleCustomized + isCustomized ); const { From c47dedd9ee4eefa9e2c1cd95834402973e56819c Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Tue, 4 Mar 2025 17:45:47 -0500 Subject: [PATCH 18/18] addresses comments --- .../missing_base_version_callout.tsx | 16 ++++++++ .../rule_upgrade/rule_upgrade.tsx | 8 +++- .../rule_upgrade/rule_upgrade_callout.tsx | 19 +-------- .../hooks/use_prebuilt_rules_upgrade.tsx | 5 ++- .../rule_update_callouts.tsx | 39 +++++++++---------- .../pages/rule_management/index.tsx | 3 +- .../create_upgradeable_rules_payload.ts | 6 +-- 7 files changed, 50 insertions(+), 46 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/missing_base_version_callout.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/missing_base_version_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/missing_base_version_callout.tsx new file mode 100644 index 0000000000000..d32007b7aa820 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/missing_base_version_callout.tsx @@ -0,0 +1,16 @@ +/* + * 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 React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import * as i18n from './translations'; + +export const RuleHasMissingBaseVersionCallout = () => ( + +

{i18n.RULE_BASE_VERSION_IS_MISSING_DESCRIPTION}

+
+); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx index 8adef539f9987..1ff08ee9b3f74 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx @@ -18,6 +18,7 @@ import { RuleUpgradeInfoBar } from './rule_upgrade_info_bar'; import { RuleUpgradeCallout } from './rule_upgrade_callout'; import { FieldUpgrade } from './field_upgrade'; import { FieldUpgradeContextProvider } from './field_upgrade_context'; +import { RuleHasMissingBaseVersionCallout } from './missing_base_version_callout'; interface RuleUpgradeProps { ruleUpgradeState: RuleUpgradeState; @@ -45,10 +46,15 @@ export const RuleUpgrade = memo(function RuleUpgrade({ targetVersionNumber={ruleUpgradeState.target_rule.version} /> + {!ruleUpgradeState.has_base_version && ( + <> + + + + )} {fieldNames.map((fieldName) => ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade_callout.tsx index 08a313eb93ccf..3d3480e472346 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade_callout.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { ActionRequiredBadge } from '../badges/action_required'; import { ReviewRequiredBadge } from '../badges/review_required_badge'; import { ReadyForUpgradeBadge } from '../badges/ready_for_upgrade_badge'; @@ -15,30 +15,15 @@ import * as i18n from './translations'; interface RuleUpgradeCalloutProps { numOfSolvableConflicts: number; numOfNonSolvableConflicts: number; - hasBaseVersion: boolean; } export function RuleUpgradeCallout({ numOfSolvableConflicts, numOfNonSolvableConflicts, - hasBaseVersion, }: RuleUpgradeCalloutProps): JSX.Element { - let missingBaseVersionCallout: JSX.Element | null = null; - if (!hasBaseVersion) { - missingBaseVersionCallout = ( - <> - -

{i18n.RULE_BASE_VERSION_IS_MISSING_DESCRIPTION}

-
- - - ); - } - if (numOfNonSolvableConflicts > 0) { return ( <> - {missingBaseVersionCallout} @@ -66,7 +51,6 @@ export function RuleUpgradeCallout({ if (numOfSolvableConflicts > 0) { return ( <> - {missingBaseVersionCallout} @@ -92,7 +76,6 @@ export function RuleUpgradeCallout({ return ( <> - {missingBaseVersionCallout} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx index 053351836518c..c7a292d580908 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx @@ -35,13 +35,14 @@ import { RuleTypeChangeCallout } from '../../rule_management_ui/components/rules import { RuleDiffTab } from '../components/rule_details/rule_diff_tab'; import { useRulePreviewFlyout } from '../../rule_management_ui/components/rules_table/use_rule_preview_flyout'; import type { UpgradePrebuiltRulesSortingOptions } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context'; +import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../../rule_management_ui/components/rules_table/constants'; const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; export interface UsePrebuiltRulesUpgradeParams { - pagination: { + pagination?: { page: number; perPage: number; }; @@ -51,7 +52,7 @@ export interface UsePrebuiltRulesUpgradeParams { } export function usePrebuiltRulesUpgrade({ - pagination, + pagination = { page: 1, perPage: RULES_TABLE_INITIAL_PAGE_SIZE }, sort, filter, onUpgrade, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx index 51467fdd0ead7..360c32e817ff6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import React, { useCallback } from 'react'; import { SecurityPageName } from '../../../../app/types'; import { useGetSecuritySolutionLinkProps } from '../../../../common/components/links'; @@ -50,25 +50,22 @@ export const RuleUpdateCallouts = ({ }, [navigateToUrl, href]); return ( - <> - - {shouldDisplayUpdateRulesCallout && ( - - )} - {shouldDisplayNewRulesCallout && ( - - )} - - - + + {shouldDisplayUpdateRulesCallout && ( + + )} + {shouldDisplayNewRulesCallout && ( + + )} + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index 12cd645f2cee4..e64ecb9173e3c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiSpacer } from '@elastic/eui'; import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared'; import React, { useCallback } from 'react'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; @@ -170,6 +170,7 @@ const RulesPageComponent: React.FC = () => {
+