From d9bc1a4b225aca439afd5581eb00d77f521a02d7 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:21:39 -0500 Subject: [PATCH] [Security Solution] Rule Diff Phase 2 components (#174564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Addresses https://github.com/elastic/kibana/issues/166489 Docs issue: https://github.com/elastic/security-docs/issues/4783 Adds per-field diffs for the rule upgrade flyout ### Acceptance Criteria - [x] The tab with per-field diffs is hidden behind a new feature flag. When the flag is off, the tab does not appear in the flyout. The tab should work regardless of the value of `jsonPrebuiltRulesDiffingEnabled`. - [x] Per-field diffs are read-only components. We don't need to let the user "merge" differences using these components. - [x] Diffs for complex fields are rendered as JSON diffs using the same component used for rendering the JSON diff for the whole rule. This means this component should be abstracted away and should accept `unknown` values in props instead of `RuleResponse`. - [x] Diffs for related fields are grouped or rendered close to each other. For example: - [x] Index patterns + Data view id - [x] Custom query + Filters + Language + Saved query id - [x] The tab uses the response from the `upgrade/_review` API endpoint and doesn't need any other API calls to render itself. - [x] The tab renders itself under 150ms. ### Screenshots Screenshot 2024-02-07 at 1 36 34 AM Screenshot 2024-02-07 at 1 36 52 AM ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: jpdjere --- .../model/diff/diffable_rule/diffable_rule.ts | 2 + .../model/diff/rule_diff/rule_diff.ts | 23 + .../common/experimental_features.ts | 15 + .../components/rule_details/constants.ts | 68 +++ .../diff_components/field_diff.tsx | 66 +++ .../diff_components/header_bar.tsx | 60 +++ .../rule_details/diff_components/index.ts | 11 + .../diff_components/panel_wrapper.tsx | 43 ++ .../diff_components/rule_diff_section.tsx | 43 ++ .../diff_components/translations.ts | 73 +++ .../components/rule_details/helpers.ts | 38 ++ .../rule_details/json_diff/hunks.tsx | 6 + .../rule_details/json_diff/translations.ts | 20 +- .../get_field_diffs_for_grouped_fields.ts | 460 ++++++++++++++++++ .../get_formatted_field_diff.ts | 133 +++++ .../rule_details/per_field_rule_diff_tab.tsx | 59 +++ .../components/rule_details/rule_diff_tab.tsx | 2 +- .../components/rule_details/translations.ts | 7 + .../model/rule_details/rule_field_diff.ts | 23 + .../upgrade_prebuilt_rules_table_context.tsx | 52 +- .../calculation/calculate_rule_fields_diff.ts | 1 + 21 files changed, 1187 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/header_bar.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/index.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/panel_wrapper.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/rule_diff_section.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/translations.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_field_diffs_for_grouped_fields.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_formatted_field_diff.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/model/rule_details/rule_field_diff.ts diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts index ac53a3c695d47..9bb6fc10031d2 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts @@ -239,6 +239,7 @@ export const DiffableNewTermsFields = buildSchema({ * NOTE: Every top-level field in a DiffableRule MUST BE LOGICALLY INDEPENDENT from other * top-level fields. */ + export type DiffableRule = t.TypeOf; export const DiffableRule = t.intersection([ DiffableCommonFields, @@ -262,6 +263,7 @@ export type DiffableAllFields = DiffableCommonFields & Omit & Omit & Omit & + Omit & Omit & Omit & Omit & diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/rule_diff/rule_diff.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/rule_diff/rule_diff.ts index 91f8b39ff70e0..d990bb3dbf461 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/rule_diff/rule_diff.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/rule_diff/rule_diff.ts @@ -73,3 +73,26 @@ export interface PartialRuleDiff { fields: Partial; has_conflict: boolean; } + +export type RuleFieldsDiffWithDataSource = + | CustomQueryFieldsDiff + | SavedQueryFieldsDiff + | EqlFieldsDiff + | ThreatMatchFieldsDiff + | ThresholdFieldsDiff + | NewTermsFieldsDiff; + +export type RuleFieldsDiffWithKqlQuery = + | CustomQueryFieldsDiff + | SavedQueryFieldsDiff + | ThreatMatchFieldsDiff + | ThresholdFieldsDiff + | NewTermsFieldsDiff; + +export type RuleFieldsDiffWithEqlQuery = EqlFieldsDiff; + +export type RuleFieldsDiffWithEsqlQuery = EsqlFieldsDiff; + +export type RuleFieldsDiffWithThreatQuery = ThreatMatchFieldsDiff; + +export type RuleFieldsDiffWithThreshold = ThresholdFieldsDiff; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index e7c5cf104b7c7..2cbcdfc9416bb 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -178,6 +178,12 @@ export const allowedExperimentalValues = Object.freeze({ * Enables experimental "Updates" tab in the prebuilt rule upgrade flyout. * This tab shows the JSON diff between the installed prebuilt rule * version and the latest available version. + * + * Ticket: https://github.com/elastic/kibana/issues/169160 + * Owners: https://github.com/orgs/elastic/teams/security-detection-rule-management + * Added: on Dec 06, 2023 in https://github.com/elastic/kibana/pull/172535 + * Turned: on Dec 20, 2023 in https://github.com/elastic/kibana/pull/173368 + * Expires: on Feb 20, 2024 */ jsonPrebuiltRulesDiffingEnabled: true, /* @@ -185,6 +191,15 @@ export const allowedExperimentalValues = Object.freeze({ * */ timelineEsqlTabDisabled: false, + + /** + * Enables per-field rule diffs tab in the prebuilt rule upgrade flyout + * + * Ticket: https://github.com/elastic/kibana/issues/166489 + * Owners: https://github.com/orgs/elastic/teams/security-detection-rule-management + * Added: on Feb 12, 2023 in https://github.com/elastic/kibana/pull/174564 + */ + perFieldPrebuiltRulesDiffingEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts index 4d6bcd542b866..45cb8f0633e65 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts @@ -5,5 +5,73 @@ * 2.0. */ +import type { DiffableAllFields } from '../../../../../common/api/detection_engine'; + export const DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['50%', '50%']; export const LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['30%', '70%']; + +export const ABOUT_UPGRADE_FIELD_ORDER: Array = [ + 'version', + 'name', + 'description', + 'author', + 'building_block', + 'severity', + 'severity_mapping', + 'risk_score', + 'risk_score_mapping', + 'references', + 'false_positives', + 'license', + 'rule_name_override', + 'threat', + 'threat_indicator_path', + 'timestamp_override', + 'tags', +]; + +export const DEFINITION_UPGRADE_FIELD_ORDER: Array = [ + 'data_source', + 'type', + 'kql_query', + 'eql_query', + 'event_category_override', + 'timestamp_field', + 'tiebreaker_field', + 'esql_query', + 'anomaly_threshold', + 'machine_learning_job_id', + 'related_integrations', + 'required_fields', + 'timeline_template', + 'threshold', + 'threat_index', + 'threat_mapping', + 'threat_query', + 'threat_indicator_path', + 'concurrent_searches', + 'items_per_search', + 'alert_suppression', + 'new_terms_fields', + 'history_window_start', + 'max_signals', +]; + +export const SCHEDULE_UPGRADE_FIELD_ORDER: Array = ['rule_schedule']; + +export const SETUP_UPGRADE_FIELD_ORDER: Array = ['setup', 'note']; + +/** + * This order is derived from a combination of the Rule Details Flyout display order + * and the `DiffableRule` type that is returned from the rule diff API endpoint + */ +export const UPGRADE_FIELD_ORDER: Array = [ + // Rule About fields + ...ABOUT_UPGRADE_FIELD_ORDER, + // Rule Definition fields + ...DEFINITION_UPGRADE_FIELD_ORDER, + // Rule Schedule fields + ...SCHEDULE_UPGRADE_FIELD_ORDER, + // Rule Setup fields + ...SETUP_UPGRADE_FIELD_ORDER, +]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx new file mode 100644 index 0000000000000..b817d6ab3882c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx @@ -0,0 +1,66 @@ +/* + * 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 { EuiFlexGroup, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; +import { camelCase, startCase } from 'lodash'; +import React from 'react'; +import { DiffView } from '../json_diff/diff_view'; +import { RuleDiffPanelWrapper } from './panel_wrapper'; +import type { FormattedFieldDiff, FieldDiff } from '../../../model/rule_details/rule_field_diff'; +import { fieldToDisplayNameMap } from './translations'; + +const SubFieldComponent = ({ + currentVersion, + targetVersion, + fieldName, + shouldShowSeparator, + shouldShowSubtitles, +}: FieldDiff & { + shouldShowSeparator: boolean; + shouldShowSubtitles: boolean; +}) => ( + + + {shouldShowSubtitles ? ( + +

{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}

+
+ ) : null} + + {shouldShowSeparator ? : null} +
+
+); + +export interface FieldDiffComponentProps { + ruleDiffs: FormattedFieldDiff; + fieldsGroupName: string; +} + +export const FieldGroupDiffComponent = ({ + ruleDiffs, + fieldsGroupName, +}: FieldDiffComponentProps) => { + const { fieldDiffs, shouldShowSubtitles } = ruleDiffs; + return ( + + {fieldDiffs.map(({ currentVersion, targetVersion, fieldName: specificFieldName }, index) => { + const shouldShowSeparator = index !== fieldDiffs.length - 1; + return ( + + ); + })} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/header_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/header_bar.tsx new file mode 100644 index 0000000000000..2f1e7f0207ecd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/header_bar.tsx @@ -0,0 +1,60 @@ +/* + * 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 { + EuiFlexGroup, + EuiHorizontalRule, + EuiIconTip, + EuiSpacer, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import React from 'react'; +import { css } from '@emotion/css'; +import * as i18n from '../json_diff/translations'; + +export const RuleDiffHeaderBar = () => { + const { euiTheme } = useEuiTheme(); + return ( +
+ + + + + +
{i18n.CURRENT_RULE_VERSION}
+
+
+ + + +
{i18n.ELASTIC_UPDATE_VERSION}
+
+
+
+ +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/index.ts new file mode 100644 index 0000000000000..6effbcf3af931 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './field_diff'; +export * from './header_bar'; +export * from './panel_wrapper'; +export * from './rule_diff_section'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/panel_wrapper.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/panel_wrapper.tsx new file mode 100644 index 0000000000000..789e9abf43395 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/panel_wrapper.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiAccordion, EuiSplitPanel, EuiTitle, useEuiTheme } from '@elastic/eui'; +import { camelCase, startCase } from 'lodash'; +import { css } from '@emotion/css'; +import React from 'react'; +import { fieldToDisplayNameMap } from './translations'; + +interface RuleDiffPanelWrapperProps { + fieldName: string; + children: React.ReactNode; +} + +export const RuleDiffPanelWrapper = ({ fieldName, children }: RuleDiffPanelWrapperProps) => { + const { euiTheme } = useEuiTheme(); + + return ( + + +
{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}
+ + } + > + {children} +
+
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/rule_diff_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/rule_diff_section.tsx new file mode 100644 index 0000000000000..fcfdf930fbd0f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/rule_diff_section.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiAccordion, EuiSpacer, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { css } from '@emotion/css'; +import type { FieldsGroupDiff } from '../../../model/rule_details/rule_field_diff'; +import { FieldGroupDiffComponent } from './field_diff'; + +interface RuleDiffSectionProps { + title: string; + fieldGroups: FieldsGroupDiff[]; +} + +export const RuleDiffSection = ({ title, fieldGroups }: RuleDiffSectionProps) => ( + <> + + +

{title}

+ + } + > + {fieldGroups.map(({ fieldsGroupName, formattedDiffs }) => { + return ( + + + + + ); + })} +
+ +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/translations.ts new file mode 100644 index 0000000000000..d8b6503f08a11 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/translations.ts @@ -0,0 +1,73 @@ +/* + * 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'; +import { + REFERENCES_FIELD_LABEL, + RISK_SCORE_MAPPING_FIELD_LABEL, + SEVERITY_MAPPING_FIELD_LABEL, + THREAT_INDICATOR_PATH_LABEL, + INDEX_FIELD_LABEL, + DATA_VIEW_ID_FIELD_LABEL, + THREAT_FIELD_LABEL, + ANOMALY_THRESHOLD_FIELD_LABEL, + MACHINE_LEARNING_JOB_ID_FIELD_LABEL, + THREAT_INDEX_FIELD_LABEL, + THREAT_MAPPING_FIELD_LABEL, + HISTORY_WINDOW_SIZE_FIELD_LABEL, +} from '../translations'; + +/** + * Used when fields have different display names or formats than their corresponding rule object fields + */ +export const fieldToDisplayNameMap: Record = { + data_source: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRuleFields.dataSourceLabel', + { + defaultMessage: 'Data source', + } + ), + note: i18n.translate('xpack.securitySolution.detectionEngine.rules.upgradeRuleFields.noteLabel', { + defaultMessage: 'Investigation guide', + }), + severity_mapping: SEVERITY_MAPPING_FIELD_LABEL, + risk_score_mapping: RISK_SCORE_MAPPING_FIELD_LABEL, + references: REFERENCES_FIELD_LABEL, + threat_indicator_path: THREAT_INDICATOR_PATH_LABEL, + index_patterns: INDEX_FIELD_LABEL, + data_view_id: DATA_VIEW_ID_FIELD_LABEL, + threat: THREAT_FIELD_LABEL, + eql_query: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRuleFields.eqlQueryLabel', + { + defaultMessage: 'EQL query', + } + ), + kql_query: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRuleFields.kqlQueryLabel', + { + defaultMessage: 'KQL query', + } + ), + threat_query: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRuleFields.threatQueryLabel', + { + defaultMessage: 'Indicator index query', + } + ), + esql_query: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRuleFields.esqlQueryLabel', + { + defaultMessage: 'ESQL query', + } + ), + anomaly_threshold: ANOMALY_THRESHOLD_FIELD_LABEL, + machine_learning_job_id: MACHINE_LEARNING_JOB_ID_FIELD_LABEL, + threat_index: THREAT_INDEX_FIELD_LABEL, + threat_mapping: THREAT_MAPPING_FIELD_LABEL, + history_window_start: HISTORY_WINDOW_SIZE_FIELD_LABEL, +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts new file mode 100644 index 0000000000000..f08187800789d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FieldsGroupDiff } from '../../model/rule_details/rule_field_diff'; +import { + ABOUT_UPGRADE_FIELD_ORDER, + DEFINITION_UPGRADE_FIELD_ORDER, + SCHEDULE_UPGRADE_FIELD_ORDER, + SETUP_UPGRADE_FIELD_ORDER, +} from './constants'; + +export const getSectionedFieldDiffs = (fields: FieldsGroupDiff[]) => { + const aboutFields = []; + const definitionFields = []; + const scheduleFields = []; + const setupFields = []; + for (const field of fields) { + if (ABOUT_UPGRADE_FIELD_ORDER.includes(field.fieldsGroupName)) { + aboutFields.push(field); + } else if (DEFINITION_UPGRADE_FIELD_ORDER.includes(field.fieldsGroupName)) { + definitionFields.push(field); + } else if (SCHEDULE_UPGRADE_FIELD_ORDER.includes(field.fieldsGroupName)) { + scheduleFields.push(field); + } else if (SETUP_UPGRADE_FIELD_ORDER.includes(field.fieldsGroupName)) { + setupFields.push(field); + } + } + return { + aboutFields, + definitionFields, + scheduleFields, + setupFields, + }; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/hunks.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/hunks.tsx index a1bada553bab7..ebff2d04cf6b2 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/hunks.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/hunks.tsx @@ -93,6 +93,12 @@ export const Hunks = ({ hunks, oldSource, expandRange }: HunksProps) => { const hunkElements = hunks.reduce((children: ReactElement[], hunk: HunkData, index: number) => { const previousElement = children[children.length - 1]; + // If old source doesn't exist, we don't render expandable sections + if (!oldSource) { + children.push(); + return children; + } + children.push( { + if (!fieldValue) { + return ''; // The JSON diff package we use does not accept `undefined` as a valid entry, empty string renders the equivalent of no field + } + + if (typeof fieldValue === 'string') { + return fieldValue; + } + return stringify(fieldValue, { space: 2 }); +}; + +export const getFieldDiffsForDataSource = ( + dataSourceThreeWayDiff: AllFieldsDiff['data_source'] +): FieldDiff[] => { + const currentType = sortAndStringifyJson(dataSourceThreeWayDiff.current_version?.type); + const targetType = sortAndStringifyJson(dataSourceThreeWayDiff.target_version?.type); + + const currentIndexPatterns = sortAndStringifyJson( + dataSourceThreeWayDiff.current_version?.type === 'index_patterns' && + dataSourceThreeWayDiff.current_version?.index_patterns + ); + const targetIndexPatterns = sortAndStringifyJson( + dataSourceThreeWayDiff.target_version?.type === 'index_patterns' && + dataSourceThreeWayDiff.target_version?.index_patterns + ); + const currentDataViewId = sortAndStringifyJson( + dataSourceThreeWayDiff.current_version?.type === 'data_view' && + dataSourceThreeWayDiff.current_version?.data_view_id + ); + const targetDataViewId = sortAndStringifyJson( + dataSourceThreeWayDiff.target_version?.type === 'data_view' && + dataSourceThreeWayDiff.target_version?.data_view_id + ); + + const hasTypeChanged = currentType !== targetType; + return [ + ...(hasTypeChanged + ? [ + { + fieldName: 'type', + currentVersion: currentType, + targetVersion: targetType, + }, + ] + : []), + ...(currentIndexPatterns !== targetIndexPatterns + ? [ + { + fieldName: 'index_patterns', + currentVersion: currentIndexPatterns, + targetVersion: targetIndexPatterns, + }, + ] + : []), + ...(currentDataViewId !== targetDataViewId + ? [ + { + fieldName: 'data_view_id', + currentVersion: currentDataViewId, + targetVersion: targetDataViewId, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForKqlQuery = ( + kqlQueryThreeWayDiff: RuleFieldsDiffWithKqlQuery['kql_query'] +): FieldDiff[] => { + const currentType = sortAndStringifyJson(kqlQueryThreeWayDiff.current_version?.type); + const targetType = sortAndStringifyJson(kqlQueryThreeWayDiff.target_version?.type); + + const currentQuery = sortAndStringifyJson( + kqlQueryThreeWayDiff.current_version?.type === 'inline_query' && + kqlQueryThreeWayDiff.current_version?.query + ); + const targetQuery = sortAndStringifyJson( + kqlQueryThreeWayDiff.target_version?.type === 'inline_query' && + kqlQueryThreeWayDiff.target_version?.query + ); + + const currentLanguage = sortAndStringifyJson( + kqlQueryThreeWayDiff.current_version?.type === 'inline_query' && + kqlQueryThreeWayDiff.current_version?.language + ); + const targetLanguage = sortAndStringifyJson( + kqlQueryThreeWayDiff.target_version?.type === 'inline_query' && + kqlQueryThreeWayDiff.target_version?.language + ); + + const currentFilters = sortAndStringifyJson( + kqlQueryThreeWayDiff.current_version?.type === 'inline_query' && + kqlQueryThreeWayDiff.current_version?.filters + ); + const targetFilters = sortAndStringifyJson( + kqlQueryThreeWayDiff.target_version?.type === 'inline_query' && + kqlQueryThreeWayDiff.target_version?.filters + ); + + const currentSavedQueryId = sortAndStringifyJson( + kqlQueryThreeWayDiff.current_version?.type === 'saved_query' && + kqlQueryThreeWayDiff.current_version?.saved_query_id + ); + const targetSavedQueryId = sortAndStringifyJson( + kqlQueryThreeWayDiff.target_version?.type === 'saved_query' && + kqlQueryThreeWayDiff.target_version?.saved_query_id + ); + + const hasTypeChanged = currentType !== targetType; + + return [ + ...(hasTypeChanged + ? [ + { + fieldName: 'type', + currentVersion: currentType, + targetVersion: targetType, + }, + ] + : []), + ...(currentQuery !== targetQuery + ? [ + { + fieldName: 'query', + currentVersion: currentQuery, + targetVersion: targetQuery, + }, + ] + : []), + ...(currentLanguage !== targetLanguage + ? [ + { + fieldName: 'language', + currentVersion: currentLanguage, + targetVersion: targetLanguage, + }, + ] + : []), + ...(currentFilters !== targetFilters + ? [ + { + fieldName: 'filters', + currentVersion: currentFilters, + targetVersion: targetFilters, + }, + ] + : []), + ...(currentSavedQueryId !== targetSavedQueryId + ? [ + { + fieldName: 'saved_query_id', + currentVersion: currentSavedQueryId, + targetVersion: targetSavedQueryId, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForEqlQuery = (eqlQuery: AllFieldsDiff['eql_query']): FieldDiff[] => { + const currentQuery = sortAndStringifyJson(eqlQuery.current_version?.query); + const targetQuery = sortAndStringifyJson(eqlQuery.target_version?.query); + + const currentFilters = sortAndStringifyJson(eqlQuery.current_version?.filters); + const targetFilters = sortAndStringifyJson(eqlQuery.target_version?.filters); + return [ + ...(currentQuery !== targetQuery + ? [ + { + fieldName: 'query', + currentVersion: currentQuery, + targetVersion: targetQuery, + }, + ] + : []), + ...(currentFilters !== targetFilters + ? [ + { + fieldName: 'filters', + currentVersion: currentFilters, + targetVersion: targetFilters, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForEsqlQuery = (esqlQuery: AllFieldsDiff['esql_query']): FieldDiff[] => { + const currentQuery = sortAndStringifyJson(esqlQuery.current_version?.query); + const targetQuery = sortAndStringifyJson(esqlQuery.target_version?.query); + + const currentLanguage = sortAndStringifyJson(esqlQuery.current_version?.language); + const targetLanguage = sortAndStringifyJson(esqlQuery.target_version?.language); + return [ + ...(currentQuery !== targetQuery + ? [ + { + fieldName: 'query', + currentVersion: currentQuery, + targetVersion: targetQuery, + }, + ] + : []), + ...(currentLanguage !== targetLanguage + ? [ + { + fieldName: 'language', + currentVersion: currentLanguage, + targetVersion: targetLanguage, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForThreatQuery = ( + threatQuery: AllFieldsDiff['threat_query'] +): FieldDiff[] => { + const currentQuery = sortAndStringifyJson(threatQuery.current_version?.query); + const targetQuery = sortAndStringifyJson(threatQuery.target_version?.query); + + const currentLanguage = sortAndStringifyJson(threatQuery.current_version?.language); + const targetLanguage = sortAndStringifyJson(threatQuery.target_version?.language); + + const currentFilters = sortAndStringifyJson(threatQuery.current_version?.filters); + const targetFilters = sortAndStringifyJson(threatQuery.target_version?.filters); + return [ + ...(currentQuery !== targetQuery + ? [ + { + fieldName: 'query', + currentVersion: currentQuery, + targetVersion: targetQuery, + }, + ] + : []), + ...(currentLanguage !== targetLanguage + ? [ + { + fieldName: 'language', + currentVersion: currentLanguage, + targetVersion: targetLanguage, + }, + ] + : []), + ...(currentFilters !== targetFilters + ? [ + { + fieldName: 'filters', + currentVersion: currentFilters, + targetVersion: targetFilters, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForRuleSchedule = ( + ruleScheduleThreeWayDiff: AllFieldsDiff['rule_schedule'] +): FieldDiff[] => { + return [ + ...(ruleScheduleThreeWayDiff.current_version?.interval !== + ruleScheduleThreeWayDiff.target_version?.interval + ? [ + { + fieldName: 'interval', + currentVersion: sortAndStringifyJson( + ruleScheduleThreeWayDiff.current_version?.interval + ), + targetVersion: sortAndStringifyJson(ruleScheduleThreeWayDiff.target_version?.interval), + }, + ] + : []), + ...(ruleScheduleThreeWayDiff.current_version?.lookback !== + ruleScheduleThreeWayDiff.target_version?.lookback + ? [ + { + fieldName: 'lookback', + currentVersion: sortAndStringifyJson( + ruleScheduleThreeWayDiff.current_version?.lookback + ), + targetVersion: sortAndStringifyJson(ruleScheduleThreeWayDiff.target_version?.lookback), + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForRuleNameOverride = ( + ruleNameOverrideThreeWayDiff: AllFieldsDiff['rule_name_override'] +): FieldDiff[] => { + const currentFieldName = sortAndStringifyJson( + ruleNameOverrideThreeWayDiff.current_version?.field_name + ); + const targetFieldName = sortAndStringifyJson( + ruleNameOverrideThreeWayDiff.target_version?.field_name + ); + return [ + ...(currentFieldName !== targetFieldName + ? [ + { + fieldName: 'field_name', + currentVersion: currentFieldName, + targetVersion: targetFieldName, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForTimestampOverride = ( + timestampOverrideThreeWayDiff: AllFieldsDiff['timestamp_override'] +): FieldDiff[] => { + const currentFieldName = sortAndStringifyJson( + timestampOverrideThreeWayDiff.current_version?.field_name + ); + const targetFieldName = sortAndStringifyJson( + timestampOverrideThreeWayDiff.target_version?.field_name + ); + const currentVersionFallbackDisabled = sortAndStringifyJson( + timestampOverrideThreeWayDiff.current_version?.fallback_disabled + ); + const targetVersionFallbackDisabled = sortAndStringifyJson( + timestampOverrideThreeWayDiff.target_version?.fallback_disabled + ); + + return [ + ...(currentFieldName !== targetFieldName + ? [ + { + fieldName: 'field_name', + currentVersion: currentFieldName, + targetVersion: targetFieldName, + }, + ] + : []), + ...(currentVersionFallbackDisabled !== targetVersionFallbackDisabled + ? [ + { + fieldName: 'fallback_disabled', + currentVersion: currentVersionFallbackDisabled, + targetVersion: targetVersionFallbackDisabled, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForTimelineTemplate = ( + timelineTemplateThreeWayDiff: AllFieldsDiff['timeline_template'] +): FieldDiff[] => { + const currentTimelineId = sortAndStringifyJson( + timelineTemplateThreeWayDiff.current_version?.timeline_id + ); + const targetTimelineId = sortAndStringifyJson( + timelineTemplateThreeWayDiff.target_version?.timeline_id + ); + + const currentTimelineTitle = sortAndStringifyJson( + timelineTemplateThreeWayDiff.current_version?.timeline_title + ); + const targetTimelineTitle = sortAndStringifyJson( + timelineTemplateThreeWayDiff.target_version?.timeline_title + ); + return [ + ...(currentTimelineId !== targetTimelineId + ? [ + { + fieldName: 'timeline_id', + currentVersion: currentTimelineId, + targetVersion: targetTimelineId, + }, + ] + : []), + ...(currentTimelineTitle !== targetTimelineTitle + ? [ + { + fieldName: 'timeline_title', + currentVersion: currentTimelineTitle, + targetVersion: targetTimelineTitle, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForBuildingBlock = ( + buildingBlockThreeWayDiff: AllFieldsDiff['building_block'] +): FieldDiff[] => { + const currentType = sortAndStringifyJson(buildingBlockThreeWayDiff.current_version?.type); + const targetType = sortAndStringifyJson(buildingBlockThreeWayDiff.target_version?.type); + return [ + ...(currentType !== targetType + ? [ + { + fieldName: 'type', + currentVersion: currentType, + targetVersion: targetType, + }, + ] + : []), + ]; +}; + +export const getFieldDiffsForThreshold = ( + thresholdThreeWayDiff: AllFieldsDiff['threshold'] +): FieldDiff[] => { + const currentField = sortAndStringifyJson(thresholdThreeWayDiff.current_version?.field); + const targetField = sortAndStringifyJson(thresholdThreeWayDiff.target_version?.field); + const currentValue = sortAndStringifyJson(thresholdThreeWayDiff.current_version?.value); + const targetValue = sortAndStringifyJson(thresholdThreeWayDiff.target_version?.value); + const currentCardinality = sortAndStringifyJson( + thresholdThreeWayDiff.current_version?.cardinality + ); + const targetCardinality = sortAndStringifyJson(thresholdThreeWayDiff.target_version?.cardinality); + + return [ + ...(currentField !== targetField + ? [ + { + fieldName: 'field', + currentVersion: currentField, + targetVersion: targetField, + }, + ] + : []), + ...(currentValue !== targetValue + ? [ + { + fieldName: 'value', + currentVersion: currentValue, + targetVersion: targetValue, + }, + ] + : []), + ...(currentCardinality !== targetCardinality + ? [ + { + fieldName: 'cardinality', + currentVersion: currentCardinality, + targetVersion: targetCardinality, + }, + ] + : []), + ]; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_formatted_field_diff.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_formatted_field_diff.ts new file mode 100644 index 0000000000000..8f150efbf6677 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_formatted_field_diff.ts @@ -0,0 +1,133 @@ +/* + * 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 { + AllFieldsDiff, + RuleFieldsDiff, + RuleFieldsDiffWithDataSource, + RuleFieldsDiffWithKqlQuery, + RuleFieldsDiffWithEqlQuery, + RuleFieldsDiffWithThreshold, + RuleFieldsDiffWithEsqlQuery, + RuleFieldsDiffWithThreatQuery, +} from '../../../../../../common/api/detection_engine'; +import type { FormattedFieldDiff } from '../../../model/rule_details/rule_field_diff'; +import { + getFieldDiffsForDataSource, + getFieldDiffsForKqlQuery, + getFieldDiffsForEqlQuery, + getFieldDiffsForRuleSchedule, + getFieldDiffsForRuleNameOverride, + getFieldDiffsForTimestampOverride, + getFieldDiffsForTimelineTemplate, + getFieldDiffsForBuildingBlock, + sortAndStringifyJson, + getFieldDiffsForThreshold, + getFieldDiffsForEsqlQuery, + getFieldDiffsForThreatQuery, +} from './get_field_diffs_for_grouped_fields'; + +export const getFormattedFieldDiffGroups = ( + fieldName: keyof AllFieldsDiff, + fields: Partial +): FormattedFieldDiff => { + /** + * Field types that contain groupings of rule fields must be formatted differently to compare and render + * each individual nested field and to satisfy types + * + * Setting shouldShowSubtitles to `true` displays the grouped field names in the rendered diff component + */ + switch (fieldName) { + case 'data_source': + const dataSourceThreeWayDiff = (fields as RuleFieldsDiffWithDataSource)[fieldName]; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForDataSource(dataSourceThreeWayDiff), + }; + case 'kql_query': + const kqlQueryThreeWayDiff = (fields as RuleFieldsDiffWithKqlQuery)[fieldName]; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForKqlQuery(kqlQueryThreeWayDiff), + }; + case 'eql_query': + const eqlQueryThreeWayDiff = (fields as RuleFieldsDiffWithEqlQuery)[fieldName]; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForEqlQuery(eqlQueryThreeWayDiff), + }; + case 'esql_query': + const esqlQueryThreeWayDiff = (fields as RuleFieldsDiffWithEsqlQuery)[fieldName]; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForEsqlQuery(esqlQueryThreeWayDiff), + }; + case 'threat_query': + const threatQueryThreeWayDiff = (fields as RuleFieldsDiffWithThreatQuery)[fieldName]; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForThreatQuery(threatQueryThreeWayDiff), + }; + case 'rule_schedule': + const ruleScheduleThreeWayDiff = fields[fieldName] as AllFieldsDiff['rule_schedule']; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForRuleSchedule(ruleScheduleThreeWayDiff), + }; + case 'rule_name_override': + const ruleNameOverrideThreeWayDiff = fields[fieldName] as AllFieldsDiff['rule_name_override']; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForRuleNameOverride(ruleNameOverrideThreeWayDiff), + }; + case 'timestamp_override': + const timestampOverrideThreeWayDiff = fields[ + fieldName + ] as AllFieldsDiff['timestamp_override']; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForTimestampOverride(timestampOverrideThreeWayDiff), + }; + case 'timeline_template': + const timelineTemplateThreeWayDiff = fields[fieldName] as AllFieldsDiff['timeline_template']; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForTimelineTemplate(timelineTemplateThreeWayDiff), + }; + case 'building_block': + const buildingBlockThreeWayDiff = fields[fieldName] as AllFieldsDiff['building_block']; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForBuildingBlock(buildingBlockThreeWayDiff), + }; + case 'threshold': + const thresholdThreeWayDiff = (fields as RuleFieldsDiffWithThreshold)[ + fieldName + ] as AllFieldsDiff['threshold']; + return { + shouldShowSubtitles: true, + fieldDiffs: getFieldDiffsForThreshold(thresholdThreeWayDiff), + }; + default: + const fieldThreeWayDiff = (fields as AllFieldsDiff)[fieldName]; + const currentVersionField = sortAndStringifyJson(fieldThreeWayDiff.current_version); + const targetVersionField = sortAndStringifyJson(fieldThreeWayDiff.target_version); + return { + shouldShowSubtitles: false, + fieldDiffs: + currentVersionField !== targetVersionField + ? [ + { + fieldName, + currentVersion: currentVersionField, + targetVersion: targetVersionField, + }, + ] + : [], + }; + } +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx new file mode 100644 index 0000000000000..2e39428a0dc12 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx @@ -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 React, { useMemo } from 'react'; +import type { PartialRuleDiff, RuleFieldsDiff } from '../../../../../common/api/detection_engine'; +import { getFormattedFieldDiffGroups } from './per_field_diff/get_formatted_field_diff'; +import { UPGRADE_FIELD_ORDER } from './constants'; +import { RuleDiffHeaderBar, RuleDiffSection } from './diff_components'; +import { getSectionedFieldDiffs } from './helpers'; +import type { FieldsGroupDiff } from '../../model/rule_details/rule_field_diff'; +import * as i18n from './translations'; + +interface PerFieldRuleDiffTabProps { + ruleDiff: PartialRuleDiff; +} + +export const PerFieldRuleDiffTab = ({ ruleDiff }: PerFieldRuleDiffTabProps) => { + const fieldsToRender = useMemo(() => { + const fields: FieldsGroupDiff[] = []; + for (const field of Object.keys(ruleDiff.fields)) { + const typedField = field as keyof RuleFieldsDiff; + const formattedDiffs = getFormattedFieldDiffGroups(typedField, ruleDiff.fields); + fields.push({ formattedDiffs, fieldsGroupName: typedField }); + } + const sortedFields = fields.sort( + (a, b) => + UPGRADE_FIELD_ORDER.indexOf(a.fieldsGroupName) - + UPGRADE_FIELD_ORDER.indexOf(b.fieldsGroupName) + ); + return sortedFields; + }, [ruleDiff.fields]); + + const { aboutFields, definitionFields, scheduleFields, setupFields } = useMemo( + () => getSectionedFieldDiffs(fieldsToRender), + [fieldsToRender] + ); + + return ( + <> + + {aboutFields.length !== 0 && ( + + )} + {definitionFields.length !== 0 && ( + + )} + {scheduleFields.length !== 0 && ( + + )} + {setupFields.length !== 0 && ( + + )} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx index fc1fa754f9694..d801f46e85696 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx @@ -167,7 +167,7 @@ export const RuleDiffTab = ({ oldRule, newRule }: RuleDiffTabProps) => { size="m" /> -
{i18n.UPDATED_VERSION}
+
{i18n.ELASTIC_UPDATE_VERSION}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts index dffdf89d31feb..9bc90aee27717 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts @@ -28,6 +28,13 @@ export const UPDATES_TAB_LABEL = i18n.translate( } ); +export const JSON_VIEW_UPDATES_TAB_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.jsonViewUpdatesTabLabel', + { + defaultMessage: 'JSON view', + } +); + export const DISMISS_BUTTON_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.dismissButtonLabel', { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/rule_details/rule_field_diff.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/rule_details/rule_field_diff.ts new file mode 100644 index 0000000000000..b5121d182e34f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/rule_details/rule_field_diff.ts @@ -0,0 +1,23 @@ +/* + * 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 { AllFieldsDiff } from '../../../../../common/api/detection_engine'; + +export interface FieldDiff { + currentVersion: string; + targetVersion: string; + fieldName: string; +} +export interface FormattedFieldDiff { + shouldShowSubtitles: boolean; + fieldDiffs: FieldDiff[]; +} + +export interface FieldsGroupDiff { + formattedDiffs: FormattedFieldDiff; + fieldsGroupName: keyof AllFieldsDiff; +} diff --git a/x-pack/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/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 4931943c3c114..d5a5938688925 100644 --- a/x-pack/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/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -9,6 +9,7 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { EuiButton } from '@elastic/eui'; import type { EuiTabbedContentTab } from '@elastic/eui'; +import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs'; import { useBoolState } from '../../../../../common/hooks/use_bool_state'; @@ -121,6 +122,10 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ 'jsonPrebuiltRulesDiffingEnabled' ); + const isPerFieldPrebuiltRulesDiffingEnabled = useIsExperimentalFeatureEnabled( + 'perFieldPrebuiltRulesDiffingEnabled' + ); + const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); const { @@ -268,27 +273,46 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ ]); const extraTabs = useMemo(() => { - const activeRule = - isJsonPrebuiltRulesDiffingEnabled && - previewedRule && - filteredRules.find(({ id }) => id === previewedRule.id); + const activeRule = previewedRule && filteredRules.find(({ id }) => id === previewedRule.id); if (!activeRule) { return []; } return [ - { - id: 'updates', - name: ruleDetailsI18n.UPDATES_TAB_LABEL, - content: ( - - - - ), - }, + ...(isPerFieldPrebuiltRulesDiffingEnabled + ? [ + { + id: 'updates', + name: ruleDetailsI18n.UPDATES_TAB_LABEL, + content: ( + + + + ), + }, + ] + : []), + ...(isJsonPrebuiltRulesDiffingEnabled + ? [ + { + id: 'jsonViewUpdates', + name: ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL, + content: ( + + + + ), + }, + ] + : []), ]; - }, [previewedRule, filteredRules, isJsonPrebuiltRulesDiffingEnabled]); + }, [ + previewedRule, + filteredRules, + isJsonPrebuiltRulesDiffingEnabled, + isPerFieldPrebuiltRulesDiffingEnabled, + ]); return ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts index 5fbb874bb71cb..5639cc07ebedd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts @@ -318,6 +318,7 @@ const allFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { ...customQueryFieldsDiffAlgorithms, ...savedQueryFieldsDiffAlgorithms, ...eqlFieldsDiffAlgorithms, + ...esqlFieldsDiffAlgorithms, ...threatMatchFieldsDiffAlgorithms, ...thresholdFieldsDiffAlgorithms, ...machineLearningFieldsDiffAlgorithms,