mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] Add ComparisonSide
component (#189384)
**Partially addresses: https://github.com/elastic/kibana/issues/171520** ## Summary This PR adds the `ComparisonSide` component for the ThreeWayDiff UI ([see it on the Miro diagram](https://miro.com/app/board/uXjVK0gqjjQ=/?moveToWidget=3458764594147853908&cot=14)). `ComparisonSide` lets the user compare field values from the two selected rule versions. It will be displayed on the left side of the upgrade flyout. You can view and test it in Storybook by running `yarn storybook security_solution` in the root Kibana dir. Go to `http://localhost:9001` once the Storybook is up and running. https://github.com/user-attachments/assets/e71ae626-d0f7-43ae-8324-f3d4ea540b02 Also updated `react-diff-view` to the latest version (`3.2.0` -> `3.2.1`)
This commit is contained in:
parent
b87e967f46
commit
3de37cea01
27 changed files with 1315 additions and 20 deletions
|
@ -1150,7 +1150,7 @@
|
|||
"re2js": "0.4.1",
|
||||
"react": "^17.0.2",
|
||||
"react-ace": "^7.0.5",
|
||||
"react-diff-view": "^3.2.0",
|
||||
"react-diff-view": "^3.2.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^4.2.9",
|
||||
"react-fast-compare": "^2.0.4",
|
||||
|
|
|
@ -25,6 +25,7 @@ import type {
|
|||
HunkTokens,
|
||||
} from 'react-diff-view';
|
||||
import unidiff from 'unidiff';
|
||||
import type { Change } from 'diff';
|
||||
import { useEuiTheme, COLOR_MODES_STANDARD } from '@elastic/eui';
|
||||
import { Hunks } from './hunks';
|
||||
import { markEdits, DiffMethod } from './mark_edits';
|
||||
|
@ -111,15 +112,32 @@ const renderGutter: RenderGutter = ({ change }) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an array of Change objects into a "unified diff" string.
|
||||
*
|
||||
* Takes an array of changes (as provided by the jsdiff library) and converts it into a "unified diff" string.
|
||||
*
|
||||
* @param {Change[]} changes - An array of changes between two strings.
|
||||
* @returns {string} A unified diff string representing the changes.
|
||||
*/
|
||||
const convertChangesToUnifiedDiffString = (changes: Change[]): string => {
|
||||
const unifiedDiff: string = unidiff.formatLines(changes, {
|
||||
context: 3,
|
||||
});
|
||||
|
||||
return unifiedDiff;
|
||||
};
|
||||
|
||||
const convertToDiffFile = (oldSource: string, newSource: string) => {
|
||||
/*
|
||||
"diffLines" call converts two strings of text into an array of Change objects.
|
||||
*/
|
||||
const changes = unidiff.diffLines(oldSource, newSource);
|
||||
const changes: Change[] = unidiff.diffLines(oldSource, newSource);
|
||||
|
||||
/*
|
||||
Then "formatLines" takes an array of Change objects and turns it into a single "unified diff" string.
|
||||
More info about the "unified diff" format: https://en.wikipedia.org/wiki/Diff_utility#Unified_format
|
||||
"convertChangesToUnifiedDiffString" converts an array of Change objects into a single "unified diff" string.
|
||||
More info about the "unified diff" format: https://en.wikipedia.org/wiki/Diff#Unified_format
|
||||
|
||||
Unified diff is a string with change markers added. Looks something like:
|
||||
`
|
||||
@@ -3,16 +3,15 @@
|
||||
|
@ -129,9 +147,7 @@ const convertToDiffFile = (oldSource: string, newSource: string) => {
|
|||
"history_window_start": "now-14d",
|
||||
`
|
||||
*/
|
||||
const unifiedDiff: string = unidiff.formatLines(changes, {
|
||||
context: 3,
|
||||
});
|
||||
const unifiedDiff: string = convertChangesToUnifiedDiffString(changes);
|
||||
|
||||
/*
|
||||
"parseDiff" converts a unified diff string into a gitdiff-parser File object.
|
||||
|
@ -154,18 +170,34 @@ const CustomStyles: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
|||
padding: 0 ${euiTheme.size.l} 0 ${euiTheme.size.m};
|
||||
}
|
||||
|
||||
/* Gutter - a narrow column on the left-hand side that displays either a "+" or a "-" */
|
||||
.${TABLE_CLASS_NAME} .diff-gutter-col {
|
||||
width: ${euiTheme.size.xl};
|
||||
}
|
||||
|
||||
/*
|
||||
Hide the redundant second gutter column in "unified" view.
|
||||
Hiding it with "display: none" would break the layout, so we set its width to 0 and make its content invisible.
|
||||
*/
|
||||
.${TABLE_CLASS_NAME}.diff-unified .diff-gutter-col + .diff-gutter-col {
|
||||
width: 0;
|
||||
}
|
||||
.${TABLE_CLASS_NAME}.diff-unified .diff-gutter + .diff-gutter {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Vertical line separating two sides of the diff view */
|
||||
.${GUTTER_CLASS_NAME}:nth-child(3) {
|
||||
border-left: 1px solid ${euiTheme.colors.mediumShade};
|
||||
}
|
||||
|
||||
.${GUTTER_CLASS_NAME}.diff-gutter-delete, .${GUTTER_CLASS_NAME}.diff-gutter-insert {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Gutter of a line with deletions */
|
||||
.${GUTTER_CLASS_NAME}.diff-gutter-delete {
|
||||
font-weight: bold;
|
||||
background: ${COLORS.light.gutterBackground.deletion};
|
||||
}
|
||||
.${DARK_THEME_CLASS_NAME} .${GUTTER_CLASS_NAME}.diff-gutter-delete {
|
||||
|
@ -174,7 +206,6 @@ const CustomStyles: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
|||
|
||||
/* Gutter of a line with insertions */
|
||||
.${GUTTER_CLASS_NAME}.diff-gutter-insert {
|
||||
font-weight: bold;
|
||||
background: ${COLORS.light.gutterBackground.insertion};
|
||||
}
|
||||
.${DARK_THEME_CLASS_NAME} .${GUTTER_CLASS_NAME}.diff-gutter-insert {
|
||||
|
@ -228,12 +259,14 @@ interface DiffViewProps extends Partial<DiffProps> {
|
|||
oldSource: string;
|
||||
newSource: string;
|
||||
diffMethod?: DiffMethod;
|
||||
viewType?: 'split' | 'unified';
|
||||
}
|
||||
|
||||
export const DiffView = ({
|
||||
oldSource,
|
||||
newSource,
|
||||
diffMethod = DiffMethod.WORDS,
|
||||
viewType = 'split',
|
||||
}: DiffViewProps) => {
|
||||
/*
|
||||
"react-diff-view" components consume diffs not as a strings, but as something they call "hunks".
|
||||
|
@ -272,6 +305,7 @@ export const DiffView = ({
|
|||
Passing 'add' or 'delete' would skip rendering one of the sides in split view.
|
||||
*/
|
||||
diffType={diffFile.type}
|
||||
viewType={viewType}
|
||||
hunks={hunks}
|
||||
renderGutter={renderGutter}
|
||||
tokens={tokens}
|
||||
|
|
|
@ -5,14 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
declare module 'unidiff' {
|
||||
interface Change {
|
||||
count?: number | undefined;
|
||||
value: string;
|
||||
added?: boolean | undefined;
|
||||
removed?: boolean | undefined;
|
||||
}
|
||||
interface Change {
|
||||
count?: number | undefined;
|
||||
value: string;
|
||||
added?: boolean | undefined;
|
||||
removed?: boolean | undefined;
|
||||
}
|
||||
|
||||
type ADDED = 'ADDED';
|
||||
type REMOVED = 'REMOVED';
|
||||
type UNMODIFIED = 'UNMODIFIED';
|
||||
|
||||
type LineChangeType = ADDED | REMOVED | UNMODIFIED;
|
||||
|
||||
declare module 'unidiff' {
|
||||
export interface FormatOptions {
|
||||
context?: number;
|
||||
}
|
||||
|
@ -21,3 +27,28 @@ declare module 'unidiff' {
|
|||
|
||||
export function formatLines(line: Change[], options?: FormatOptions): string;
|
||||
}
|
||||
|
||||
declare module 'unidiff/hunk' {
|
||||
export const ADDED: ADDED;
|
||||
export const REMOVED: REMOVED;
|
||||
export const UNMODIFIED: UNMODIFIED;
|
||||
|
||||
export type ChangeWithType = Change & { type: LineChangeType };
|
||||
|
||||
export interface LineChange {
|
||||
type: LineChangeType;
|
||||
text: string;
|
||||
unified(): string;
|
||||
}
|
||||
|
||||
export interface UniDiffHunk {
|
||||
aoff: number;
|
||||
boff: number;
|
||||
changes: LineChange[];
|
||||
unified(): string;
|
||||
}
|
||||
|
||||
export function lineChanges(change: ChangeWithType): LineChange[];
|
||||
|
||||
export function hunk(aOffset: number, bOffset: number, lchanges: LineChange[]): UniDiffHunk;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { ComparisonSide } from './comparison_side';
|
||||
import type {
|
||||
ThreeWayDiff,
|
||||
DiffableAllFields,
|
||||
RuleKqlQuery,
|
||||
} from '../../../../../../../common/api/detection_engine';
|
||||
import {
|
||||
ThreeWayDiffOutcome,
|
||||
ThreeWayMergeOutcome,
|
||||
ThreeWayDiffConflict,
|
||||
KqlQueryType,
|
||||
} from '../../../../../../../common/api/detection_engine';
|
||||
export default {
|
||||
component: ComparisonSide,
|
||||
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/ComparisonSide',
|
||||
argTypes: {
|
||||
fieldName: {
|
||||
control: 'text',
|
||||
description: 'Field name to compare',
|
||||
},
|
||||
fieldThreeWayDiff: {
|
||||
control: 'object',
|
||||
description: 'Field ThreeWayDiff',
|
||||
},
|
||||
resolvedValue: {
|
||||
control: 'text',
|
||||
description: 'Resolved value',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface TemplateProps<FieldName extends keyof DiffableAllFields> {
|
||||
fieldName: FieldName;
|
||||
fieldThreeWayDiff: ThreeWayDiff<DiffableAllFields[FieldName]>;
|
||||
resolvedValue?: DiffableAllFields[FieldName];
|
||||
}
|
||||
|
||||
const Template: Story<TemplateProps<keyof DiffableAllFields>> = (args) => {
|
||||
return (
|
||||
<ComparisonSide
|
||||
fieldName={args.fieldName}
|
||||
fieldThreeWayDiff={args.fieldThreeWayDiff}
|
||||
resolvedValue={args.resolvedValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoBaseVersion = Template.bind({});
|
||||
NoBaseVersion.args = {
|
||||
fieldName: 'rule_name_override',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
current_version: {
|
||||
field_name: 'rule.name',
|
||||
},
|
||||
target_version: undefined,
|
||||
merged_version: undefined,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResolvedValue = Template.bind({});
|
||||
WithResolvedValue.args = {
|
||||
fieldName: 'risk_score',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: true,
|
||||
base_version: 10,
|
||||
current_version: 40,
|
||||
target_version: 20,
|
||||
merged_version: 40,
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
|
||||
},
|
||||
resolvedValue: 35,
|
||||
};
|
||||
WithResolvedValue.argTypes = {
|
||||
resolvedValue: {
|
||||
control: 'number',
|
||||
},
|
||||
};
|
||||
|
||||
/* Optional field becomes defined - was undefined in base version, but was then defined by user in current version */
|
||||
export const OptionalFieldBecomesDefined = Template.bind({});
|
||||
OptionalFieldBecomesDefined.args = {
|
||||
fieldName: 'timestamp_override',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: true,
|
||||
base_version: undefined,
|
||||
current_version: {
|
||||
field_name: 'event.ingested',
|
||||
fallback_disabled: true,
|
||||
},
|
||||
target_version: undefined,
|
||||
merged_version: {
|
||||
field_name: 'event.ingested',
|
||||
fallback_disabled: true,
|
||||
},
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_update: false,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
},
|
||||
};
|
||||
|
||||
export const SubfieldsWithLabels = Template.bind({});
|
||||
|
||||
const subfieldsWithLabelsThreeWayDiff: ThreeWayDiff<RuleKqlQuery> = {
|
||||
has_base_version: true,
|
||||
base_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'event.agent_id_status: *',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
current_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'event.agent_id_status: *',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'e355ef26-45f5-40f1-bbb7-5176ecf07d5c',
|
||||
},
|
||||
merged_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'e355ef26-45f5-40f1-bbb7-5176ecf07d5c',
|
||||
},
|
||||
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
};
|
||||
|
||||
SubfieldsWithLabels.args = {
|
||||
fieldName: 'kql_query',
|
||||
fieldThreeWayDiff: subfieldsWithLabelsThreeWayDiff,
|
||||
};
|
||||
|
||||
/* Field type changes - in this example "kql_query" field was "inline" in base version, but became "saved" in the current version */
|
||||
export const FieldTypeChanges = Template.bind({});
|
||||
|
||||
const fieldTypeChangesThreeWayDiff: ThreeWayDiff<RuleKqlQuery> = {
|
||||
has_base_version: true,
|
||||
base_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'event.agent_id_status: *',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
current_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'e355ef26-45f5-40f1-bbb7-5176ecf07d5c',
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'event.agent_id_status: *',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
merged_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'e355ef26-45f5-40f1-bbb7-5176ecf07d5c',
|
||||
},
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_update: false,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
};
|
||||
|
||||
FieldTypeChanges.args = {
|
||||
fieldName: 'kql_query',
|
||||
fieldThreeWayDiff: fieldTypeChangesThreeWayDiff,
|
||||
};
|
||||
|
||||
export const SingleLineStringSubfieldChanges = Template.bind({});
|
||||
SingleLineStringSubfieldChanges.args = {
|
||||
fieldName: 'name',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: true,
|
||||
base_version: 'Prebuilt rule',
|
||||
current_version: 'Customized prebuilt rule',
|
||||
target_version: 'Prebuilt rule with new changes',
|
||||
merged_version: 'Customized prebuilt rule',
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiLineStringSubfieldChanges = Template.bind({});
|
||||
MultiLineStringSubfieldChanges.args = {
|
||||
fieldName: 'note',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: true,
|
||||
base_version: 'My description.\f\nThis is a second\u2001 line.\f\nThis is a third line.',
|
||||
current_version:
|
||||
'My GREAT description.\f\nThis is a second\u2001 line.\f\nThis is a third line.',
|
||||
target_version: 'My description.\f\nThis is a second\u2001 line.\f\nThis is a GREAT line.',
|
||||
merged_version:
|
||||
'My GREAT description.\f\nThis is a second\u2001 line.\f\nThis is a GREAT line.',
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Merged,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
},
|
||||
};
|
||||
|
||||
export const NumberSubfieldChanges = Template.bind({});
|
||||
NumberSubfieldChanges.args = {
|
||||
fieldName: 'risk_score',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: true,
|
||||
base_version: 33,
|
||||
current_version: 43,
|
||||
target_version: 53,
|
||||
merged_version: 43,
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
|
||||
},
|
||||
};
|
||||
|
||||
export const ArraySubfieldChanges = Template.bind({});
|
||||
ArraySubfieldChanges.args = {
|
||||
fieldName: 'tags',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: true,
|
||||
base_version: ['one', 'two', 'three'],
|
||||
current_version: ['two', 'three', 'four', 'five'],
|
||||
target_version: ['one', 'three', 'four', 'six'],
|
||||
merged_version: ['three', 'four', 'five', 'six'],
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
},
|
||||
};
|
||||
|
||||
export const QuerySubfieldChanges = Template.bind({});
|
||||
QuerySubfieldChanges.args = {
|
||||
fieldName: 'kql_query',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: true,
|
||||
base_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query:
|
||||
'event.action:("Directory Service Changes" or "directory-service-object-modified") and event.code:5136 and\n winlog.event_data.OperationType:"%%14674" and\n winlog.event_data.ObjectClass:"user" and\n winlog.event_data.AttributeLDAPDisplayName:"servicePrincipalName"\n',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
current_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query:
|
||||
'event.action:("Directory Service Changes" or "directory-service-object-modified") and event.code:5136 and\n winlog.event_data.OperationType:"%%14674" or winlog.event_data.OperationType:"%%14675" and\n winlog.event_data.ObjectClass:"user" and\n winlog.event_data.AttributeLDAPDisplayName:"serviceSecondaryName"\n',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query:
|
||||
'event.action:("Directory Service Changes" or "Directory Service Modifications" or "directory-service-object-modified") and event.code:5136 and\n winlog.event_data.OperationType:"%%14674" and\n winlog.event_data.ObjectClass:"user" and\n winlog.event_data.AttributeLDAPDisplayName:"servicePrincipalName"\n',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
merged_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query:
|
||||
'event.action:("Directory Service Changes" or "directory-service-object-modified") and event.code:5136 and\n winlog.event_data.OperationType:"%%14674" or winlog.event_data.OperationType:"%%14675" and\n winlog.event_data.ObjectClass:"user" and\n winlog.event_data.AttributeLDAPDisplayName:"serviceSecondaryName"\n',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
|
||||
},
|
||||
};
|
||||
|
||||
export const SetupGuideSubfieldChanges = Template.bind({});
|
||||
SetupGuideSubfieldChanges.args = {
|
||||
fieldName: 'setup',
|
||||
fieldThreeWayDiff: {
|
||||
has_base_version: true,
|
||||
base_version:
|
||||
'## Setup\n\nThis rule requires data coming in from one of the following integrations:\n- Elastic Defend\n- Auditbeat\n\n### Elastic Defend Integration Setup\nElastic Defend is integrated into the Elastic Agent using Fleet. Upon configuration, the integration allows the Elastic Agent to monitor events on your host and send data to the Elastic Security app.\n\n#### Prerequisite Requirements:\n- Fleet is required for Elastic Defend.\n- To configure Fleet Server refer to the [documentation](https://www.elastic.co/guide/en/fleet/current/fleet-server.html).\n\n#### The following steps should be executed in order to add the Elastic Defend integration on a Linux System:\n- Go to the Kibana home page and click "Add integrations".\n- In the query bar, search for "Elastic Defend" and select the integration to see more details about it.\n- Click "Add Elastic Defend".\n- Configure the integration name and optionally add a description.\n- Select the type of environment you want to protect, either "Traditional Endpoints" or "Cloud Workloads".\n- Select a configuration preset. Each preset comes with different default settings for Elastic Agent, you can further customize these later by configuring the Elastic Defend integration policy. [Helper guide](https://www.elastic.co/guide/en/security/current/configure-endpoint-integration-policy.html).\n- We suggest selecting "Complete EDR (Endpoint Detection and Response)" as a configuration setting, that provides "All events; all preventions"\n- Enter a name for the agent policy in "New agent policy name". If other agent policies already exist, you can click the "Existing hosts" tab and select an existing policy instead.\nFor more details on Elastic Agent configuration settings, refer to the [helper guide](https://www.elastic.co/guide/en/fleet/8.10/agent-policy.html).\n- Click "Save and Continue".\n- To complete the integration, select "Add Elastic Agent to your hosts" and continue to the next section to install the Elastic Agent on your hosts.\nFor more details on Elastic Defend refer to the [helper guide](https://www.elastic.co/guide/en/security/current/install-endpoint.html).\n\n### Auditbeat Setup\nAuditbeat is a lightweight shipper that you can install on your servers to audit the activities of users and processes on your systems. For example, you can use Auditbeat to collect and centralize audit events from the Linux Audit Framework. You can also use Auditbeat to detect changes to critical files, like binaries and configuration files, and identify potential security policy violations.\n\n#### The following steps should be executed in order to add the Auditbeat on a Linux System:\n- Elastic provides repositories available for APT and YUM-based distributions. Note that we provide binary packages, but no source packages.\n- To install the APT and YUM repositories follow the setup instructions in this [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/setup-repositories.html).\n- To run Auditbeat on Docker follow the setup instructions in the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/running-on-docker.html).\n- To run Auditbeat on Kubernetes follow the setup instructions in the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/running-on-kubernetes.html).\n- For complete “Setup and Run Auditbeat” information refer to the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/setting-up-and-running.html).\n',
|
||||
current_version:
|
||||
'## Setup\n\nThis rule requires data coming in from one of the following integrations:\n- Elastic Defend\n- Auditbeat\n- Custom Windows Event Logs\n\n### Elastic Defend Integration Setup\nElastic Defend is integrated into the Elastic Agent using Fleet. Upon configuration, the integration allows the Elastic Agent to monitor events on your host and send data to the Elastic Security app.\n\n#### Prerequisite Requirements:\n- Fleet is required for Elastic Defend.\n- To configure Fleet Server refer to the [documentation](https://www.elastic.co/guide/en/fleet/current/fleet-server.html).\n\n#### The following steps should be executed in order to add the Elastic Defend integration on a Linux System:\n- Go to the Kibana home page and click "Add integrations".\n- In the query bar, search for "Elastic Defend" and select the integration to see more details about it.\n- Click "Add Elastic Defend".\n- Configure the integration name and optionally add a description.\n- Select the type of environment you want to protect.\n- Select a configuration preset. Each preset comes with different default settings for Elastic Agent, you can further customize these later by configuring the Elastic Defend integration policy. [Helper guide](https://www.elastic.co/guide/en/security/current/configure-endpoint-integration-policy.html).\n- Enter a name for the agent policy in "New agent policy name". If other agent policies already exist, you can click the "Existing hosts" tab and select an existing policy instead.\nFor more details on Elastic Agent configuration settings, refer to the [helper guide](https://www.elastic.co/guide/en/fleet/8.10/agent-policy.html).\n- Click "Save and Continue".\n- The rule is now ready to run.\n- To complete the integration, select "Add Elastic Agent to your hosts" and continue to the next section to install the Elastic Agent on your hosts.\nFor more details on Elastic Defend refer to the [helper guide](https://www.elastic.co/guide/en/security/current/install-endpoint.html).\n\n### Auditbeat Setup\nAuditbeat is a lightweight shipper that you can install on your servers to audit the activities of users and processes on your systems. For example, you can use Auditbeat to collect and centralize audit events from the Linux Audit Framework. You can also use Auditbeat to detect changes to critical files, like binaries and configuration files, and identify potential security policy violations.\n\n#### The following steps should be executed in order to add the Auditbeat on a Linux System:\n- Elastic provides repositories available for AXT and YUM-based distributions. Note that we provide binary packages, but no source packages.\n- To install the AXT and YUM repositories follow the setup instructions in this [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/setup-repositories.html).\n- To run Auditbeat on Docker follow the setup instructions in the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/running-on-docker.html).\n- To run Auditbeat on Kubernetes follow the setup instructions in the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/running-on-kubernetes.html).\n- For complete “Setup and Run Auditbeat” information refer to the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/setting-up-and-running.html).\n',
|
||||
target_version:
|
||||
'## Setup\n\nThis rule requires data coming in from one of the following integrations:\n- Elastic Defend\n- Auditbeat\n\n### Elastic Defend Integration Setup\nElastic Defend is integrated into the Elastic Agent using Fleet. Upon configuration, the integration allows the Elastic Agent to monitor events on your host and send data to the Elastic Security app.\n\n#### Prerequisite Requirements:\n- Fleet is required for Elastic Defend.\n- To configure Fleet Server refer to the [documentation](https://www.elastic.co/guide/en/fleet/current/fleet-server.html).\n\n#### The following steps should be executed in order to add the Elastic Defend integration on a Linux System:\n- Go to the Kibana home page and click "Add integrations".\n- In the query bar, search for "Elastic Defend" and select the integration to see more details about it.\n- Click "Add Elastic Defend".\n- Configure the integration name and optionally add a description.\n- Select the type of environment you want to protect, either "Traditional Endpoints" or "Cloud Workloads".\n- Carefully select a configuration preset. Each preset comes with different default settings for Elastic Agent, you can further customize these later by configuring the Elastic Defend integration policy. [Helper guide](https://www.elastic.co/guide/en/security/current/configure-endpoint-integration-policy.html).\n- We suggest selecting "Complete EDR (Endpoint Detection and Response)" as a configuration setting, that provides "All events; all preventions"\n- Enter a title for the agent policy in "New agent policy title". If other agent policies already exist, you can click the "Existing hosts" tab and select an existing policy instead.\nFor more details on Elastic Agent configuration settings, refer to the [helper guide](https://www.elastic.co/guide/en/fleet/8.10/agent-policy.html).\n- Click "Save and Continue".\n- To complete the integration, select "Add Elastic Agent to your hosts" and continue to the next section to install the Elastic Agent on your hosts.\nFor more details on Elastic Defend refer to the [helper guide](https://www.elastic.co/guide/en/security/current/install-endpoint.html).\n\n### Auditbeat Setup\nAuditbeat is a lightweight shipper that you can install on your servers to audit the activities of users and processes on your systems. You can use Auditbeat to detect changes to critical files, like binaries and configuration files, and identify potential security policy violations.\n\n#### The following steps should be executed in order to add the Auditbeat on a Linux System:\n- Elastic provides repositories available for APT and YUM-based distributions. Note that we provide binary packages, but no source packages.\n- To install the APT and YUM repositories follow the setup instructions in this [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/setup-repositories.html).\n- To run Auditbeat on Docker follow the setup instructions in the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/running-on-docker.html).\n- To run Auditbeat on Kubernetes follow the setup instructions in the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/running-on-kubernetes.html).\n- For complete “Setup and Run Auditbeat” information refer to the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/setting-up-and-running.html).\n- Good luck!\n',
|
||||
merged_version:
|
||||
'## Setup\n\nThis rule requires data coming in from one of the following integrations:\n- Elastic Defend\n- Auditbeat\n- Custom Windows Event Logs\n\n### Elastic Defend Integration Setup\nElastic Defend is integrated into the Elastic Agent using Fleet. Upon configuration, the integration allows the Elastic Agent to monitor events on your host and send data to the Elastic Security app.\n\n#### Prerequisite Requirements:\n- Fleet is required for Elastic Defend.\n- To configure Fleet Server refer to the [documentation](https://www.elastic.co/guide/en/fleet/current/fleet-server.html).\n\n#### The following steps should be executed in order to add the Elastic Defend integration on a Linux System:\n- Go to the Kibana home page and click "Add integrations".\n- In the query bar, search for "Elastic Defend" and select the integration to see more details about it.\n- Click "Add Elastic Defend".\n- Configure the integration name and optionally add a description.\n- Select the type of environment you want to protect.\n- Carefully select a configuration preset. Each preset comes with different default settings for Elastic Agent, you can further customize these later by configuring the Elastic Defend integration policy. [Helper guide](https://www.elastic.co/guide/en/security/current/configure-endpoint-integration-policy.html).\n- Enter a title for the agent policy in "New agent policy title". If other agent policies already exist, you can click the "Existing hosts" tab and select an existing policy instead.\nFor more details on Elastic Agent configuration settings, refer to the [helper guide](https://www.elastic.co/guide/en/fleet/8.10/agent-policy.html).\n- Click "Save and Continue".\n- The rule is now ready to run.\n- To complete the integration, select "Add Elastic Agent to your hosts" and continue to the next section to install the Elastic Agent on your hosts.\nFor more details on Elastic Defend refer to the [helper guide](https://www.elastic.co/guide/en/security/current/install-endpoint.html).\n\n### Auditbeat Setup\nAuditbeat is a lightweight shipper that you can install on your servers to audit the activities of users and processes on your systems. You can use Auditbeat to detect changes to critical files, like binaries and configuration files, and identify potential security policy violations.\n\n#### The following steps should be executed in order to add the Auditbeat on a Linux System:\n- Elastic provides repositories available for AXT and YUM-based distributions. Note that we provide binary packages, but no source packages.\n- To install the AXT and YUM repositories follow the setup instructions in this [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/setup-repositories.html).\n- To run Auditbeat on Docker follow the setup instructions in the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/running-on-docker.html).\n- To run Auditbeat on Kubernetes follow the setup instructions in the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/running-on-kubernetes.html).\n- For complete “Setup and Run Auditbeat” information refer to the [helper guide](https://www.elastic.co/guide/en/beats/auditbeat/current/setting-up-and-running.html).\n- Good luck!\n',
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Merged,
|
||||
has_update: true,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { VersionsPicker } from '../versions_picker/versions_picker';
|
||||
import type { Version } from '../versions_picker/constants';
|
||||
import { SelectedVersions } from '../versions_picker/constants';
|
||||
import { pickFieldValueForVersion } from './utils';
|
||||
import type {
|
||||
DiffableAllFields,
|
||||
ThreeWayDiff,
|
||||
} from '../../../../../../../common/api/detection_engine';
|
||||
import { getSubfieldChanges } from './get_subfield_changes';
|
||||
import { SubfieldChanges } from './subfield_changes';
|
||||
|
||||
interface ComparisonSideProps<FieldName extends keyof DiffableAllFields> {
|
||||
fieldName: FieldName;
|
||||
fieldThreeWayDiff: ThreeWayDiff<DiffableAllFields[FieldName]>;
|
||||
resolvedValue?: DiffableAllFields[FieldName];
|
||||
}
|
||||
|
||||
export function ComparisonSide<FieldName extends keyof DiffableAllFields>({
|
||||
fieldName,
|
||||
fieldThreeWayDiff,
|
||||
resolvedValue,
|
||||
}: ComparisonSideProps<FieldName>) {
|
||||
const [selectedVersions, setSelectedVersions] = useState<SelectedVersions>(
|
||||
SelectedVersions.CurrentFinal
|
||||
);
|
||||
|
||||
const [oldVersionType, newVersionType] = selectedVersions.split('_') as [Version, Version];
|
||||
|
||||
const oldFieldValue = pickFieldValueForVersion(oldVersionType, fieldThreeWayDiff, resolvedValue);
|
||||
const newFieldValue = pickFieldValueForVersion(newVersionType, fieldThreeWayDiff, resolvedValue);
|
||||
|
||||
const subfieldChanges = getSubfieldChanges(fieldName, oldFieldValue, newFieldValue);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VersionsPicker
|
||||
hasBaseVersion={fieldThreeWayDiff.has_base_version}
|
||||
selectedVersions={selectedVersions}
|
||||
onChange={setSelectedVersions}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<SubfieldChanges fieldName={fieldName} subfieldChanges={subfieldChanges} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { DiffableAllFields } from '../../../../../../../common/api/detection_engine';
|
||||
|
||||
export const FIELDS_WITH_SUBFIELDS: Array<keyof DiffableAllFields> = [
|
||||
'data_source',
|
||||
'kql_query',
|
||||
'eql_query',
|
||||
'esql_query',
|
||||
'threat_query',
|
||||
'rule_schedule',
|
||||
'rule_name_override',
|
||||
'timestamp_override',
|
||||
'timeline_template',
|
||||
'building_block',
|
||||
'threshold',
|
||||
];
|
|
@ -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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForBuildingBlock = (
|
||||
oldFieldValue?: DiffableAllFields['building_block'],
|
||||
newFieldValue?: DiffableAllFields['building_block']
|
||||
): SubfieldChange[] => {
|
||||
const oldType = stringifyToSortedJson(oldFieldValue?.type);
|
||||
const newType = stringifyToSortedJson(newFieldValue?.type);
|
||||
|
||||
if (oldType !== newType) {
|
||||
return [
|
||||
{
|
||||
subfieldName: 'type',
|
||||
oldSubfieldValue: oldType,
|
||||
newSubfieldValue: newType,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
|
@ -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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForDataSource = (
|
||||
oldFieldValue?: DiffableAllFields['data_source'],
|
||||
newFieldValue?: DiffableAllFields['data_source']
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldType = stringifyToSortedJson(oldFieldValue?.type);
|
||||
const newType = stringifyToSortedJson(newFieldValue?.type);
|
||||
|
||||
if (oldType !== newType) {
|
||||
changes.push({
|
||||
subfieldName: 'type',
|
||||
oldSubfieldValue: oldType,
|
||||
newSubfieldValue: newType,
|
||||
});
|
||||
}
|
||||
|
||||
const oldIndexPatterns = stringifyToSortedJson(
|
||||
oldFieldValue?.type === 'index_patterns' ? oldFieldValue?.index_patterns : ''
|
||||
);
|
||||
const newIndexPatterns = stringifyToSortedJson(
|
||||
newFieldValue?.type === 'index_patterns' ? newFieldValue?.index_patterns : ''
|
||||
);
|
||||
|
||||
if (oldIndexPatterns !== newIndexPatterns) {
|
||||
changes.push({
|
||||
subfieldName: 'index_patterns',
|
||||
oldSubfieldValue: oldIndexPatterns,
|
||||
newSubfieldValue: newIndexPatterns,
|
||||
});
|
||||
}
|
||||
|
||||
const oldDataViewId = stringifyToSortedJson(
|
||||
oldFieldValue?.type === 'data_view' ? oldFieldValue?.data_view_id : ''
|
||||
);
|
||||
const newDataViewId = stringifyToSortedJson(
|
||||
newFieldValue?.type === 'data_view' ? newFieldValue?.data_view_id : ''
|
||||
);
|
||||
|
||||
if (oldDataViewId !== newDataViewId) {
|
||||
changes.push({
|
||||
subfieldName: 'data_view_id',
|
||||
oldSubfieldValue: oldDataViewId,
|
||||
newSubfieldValue: newDataViewId,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForEqlQuery = (
|
||||
oldFieldValue?: DiffableAllFields['eql_query'],
|
||||
newFieldValue?: DiffableAllFields['eql_query']
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldQuery = stringifyToSortedJson(oldFieldValue?.query);
|
||||
const newQuery = stringifyToSortedJson(newFieldValue?.query);
|
||||
|
||||
if (oldQuery !== newQuery) {
|
||||
changes.push({
|
||||
subfieldName: 'query',
|
||||
oldSubfieldValue: oldQuery,
|
||||
newSubfieldValue: newQuery,
|
||||
});
|
||||
}
|
||||
|
||||
const oldFilters = stringifyToSortedJson(oldFieldValue?.filters);
|
||||
const newFilters = stringifyToSortedJson(newFieldValue?.filters);
|
||||
|
||||
if (oldFilters !== newFilters) {
|
||||
changes.push({
|
||||
subfieldName: 'filters',
|
||||
oldSubfieldValue: oldFilters,
|
||||
newSubfieldValue: newFilters,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForEsqlQuery = (
|
||||
oldFieldValue?: DiffableAllFields['esql_query'],
|
||||
newFieldValue?: DiffableAllFields['esql_query']
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldQuery = stringifyToSortedJson(oldFieldValue?.query);
|
||||
const newQuery = stringifyToSortedJson(newFieldValue?.query);
|
||||
|
||||
if (oldQuery !== newQuery) {
|
||||
changes.push({
|
||||
subfieldName: 'query',
|
||||
oldSubfieldValue: oldQuery,
|
||||
newSubfieldValue: newQuery,
|
||||
});
|
||||
}
|
||||
|
||||
const oldLanguage = stringifyToSortedJson(oldFieldValue?.language);
|
||||
const newLanguage = stringifyToSortedJson(oldFieldValue?.language);
|
||||
|
||||
if (oldLanguage !== newLanguage) {
|
||||
changes.push({
|
||||
subfieldName: 'language',
|
||||
oldSubfieldValue: oldLanguage,
|
||||
newSubfieldValue: newLanguage,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import { stringifyToSortedJson } from '../utils';
|
||||
import { getSubfieldChangesForDataSource } from './data_source';
|
||||
import { getSubfieldChangesForKqlQuery } from './kql_query';
|
||||
import { getSubfieldChangesForEqlQuery } from './eql_query';
|
||||
import { getSubfieldChangesForEsqlQuery } from './esql_query';
|
||||
import { getSubfieldChangesForThreatQuery } from './threat_query';
|
||||
import { getSubfieldChangesForRuleSchedule } from './rule_schedule';
|
||||
import { getSubfieldChangesForRuleNameOverride } from './rule_name_override';
|
||||
import { getSubfieldChangesForTimestampOverride } from './timestamp_override';
|
||||
import { getSubfieldChangesForTimelineTemplate } from './timeline_template';
|
||||
import { getSubfieldChangesForBuildingBlock } from './building_block';
|
||||
import { getSubfieldChangesForThreshold } from './threshold';
|
||||
import type { SubfieldChanges } from '../types';
|
||||
|
||||
/**
|
||||
* Splits a field into subfields and returns the changes between the old and new subfield values.
|
||||
*
|
||||
* @param fieldName - The name of the field for which subfield changes are to be computed.
|
||||
* @param oldFieldValue - The old value of the field.
|
||||
* @param newFieldValue - The new value of the field.
|
||||
* @returns - An array of subfield changes.
|
||||
*/
|
||||
export const getSubfieldChanges = <FieldName extends keyof DiffableAllFields>(
|
||||
fieldName: FieldName,
|
||||
oldFieldValue?: DiffableAllFields[FieldName],
|
||||
newFieldValue?: DiffableAllFields[FieldName]
|
||||
): SubfieldChanges => {
|
||||
switch (fieldName) {
|
||||
/*
|
||||
Typecasting `oldFieldValue` and `newFieldValue` to corresponding field
|
||||
type `DiffableAllFields[*]` is required here since `oldFieldValue` and
|
||||
`newFieldValue` concrete types depend on `fieldName` but TS doesn't track that.
|
||||
*/
|
||||
case 'data_source':
|
||||
return getSubfieldChangesForDataSource(
|
||||
oldFieldValue as DiffableAllFields['data_source'],
|
||||
newFieldValue as DiffableAllFields['data_source']
|
||||
);
|
||||
case 'kql_query':
|
||||
return getSubfieldChangesForKqlQuery(
|
||||
oldFieldValue as DiffableAllFields['kql_query'],
|
||||
newFieldValue as DiffableAllFields['kql_query']
|
||||
);
|
||||
case 'eql_query':
|
||||
return getSubfieldChangesForEqlQuery(
|
||||
oldFieldValue as DiffableAllFields['eql_query'],
|
||||
newFieldValue as DiffableAllFields['eql_query']
|
||||
);
|
||||
case 'esql_query':
|
||||
return getSubfieldChangesForEsqlQuery(
|
||||
oldFieldValue as DiffableAllFields['esql_query'],
|
||||
newFieldValue as DiffableAllFields['esql_query']
|
||||
);
|
||||
case 'threat_query':
|
||||
return getSubfieldChangesForThreatQuery(
|
||||
oldFieldValue as DiffableAllFields['threat_query'],
|
||||
newFieldValue as DiffableAllFields['threat_query']
|
||||
);
|
||||
case 'rule_schedule':
|
||||
return getSubfieldChangesForRuleSchedule(
|
||||
oldFieldValue as DiffableAllFields['rule_schedule'],
|
||||
newFieldValue as DiffableAllFields['rule_schedule']
|
||||
);
|
||||
case 'rule_name_override':
|
||||
return getSubfieldChangesForRuleNameOverride(
|
||||
oldFieldValue as DiffableAllFields['rule_name_override'],
|
||||
newFieldValue as DiffableAllFields['rule_name_override']
|
||||
);
|
||||
case 'timestamp_override':
|
||||
return getSubfieldChangesForTimestampOverride(
|
||||
oldFieldValue as DiffableAllFields['timestamp_override'],
|
||||
newFieldValue as DiffableAllFields['timestamp_override']
|
||||
);
|
||||
case 'timeline_template':
|
||||
return getSubfieldChangesForTimelineTemplate(
|
||||
oldFieldValue as DiffableAllFields['timeline_template'],
|
||||
newFieldValue as DiffableAllFields['timeline_template']
|
||||
);
|
||||
case 'building_block':
|
||||
return getSubfieldChangesForBuildingBlock(
|
||||
oldFieldValue as DiffableAllFields['building_block'],
|
||||
newFieldValue as DiffableAllFields['building_block']
|
||||
);
|
||||
case 'threshold':
|
||||
return getSubfieldChangesForThreshold(
|
||||
oldFieldValue as DiffableAllFields['threshold'],
|
||||
newFieldValue as DiffableAllFields['threshold']
|
||||
);
|
||||
default:
|
||||
const oldFieldValueStringified = stringifyToSortedJson(oldFieldValue);
|
||||
const newFieldValueStringified = stringifyToSortedJson(newFieldValue);
|
||||
|
||||
if (oldFieldValueStringified === newFieldValueStringified) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
subfieldName: fieldName,
|
||||
oldSubfieldValue: oldFieldValueStringified,
|
||||
newSubfieldValue: newFieldValueStringified,
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { stringifyToSortedJson } from '../utils';
|
||||
import type { RuleKqlQuery } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForKqlQuery = (
|
||||
oldFieldValue?: RuleKqlQuery,
|
||||
newFieldValue?: RuleKqlQuery
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldType = stringifyToSortedJson(oldFieldValue?.type);
|
||||
const newType = stringifyToSortedJson(newFieldValue?.type);
|
||||
|
||||
if (oldType !== newType) {
|
||||
changes.push({
|
||||
subfieldName: 'type',
|
||||
oldSubfieldValue: oldType,
|
||||
newSubfieldValue: newType,
|
||||
});
|
||||
}
|
||||
|
||||
const oldQuery = stringifyToSortedJson(
|
||||
oldFieldValue?.type === 'inline_query' ? oldFieldValue?.query : ''
|
||||
);
|
||||
const newQuery = stringifyToSortedJson(
|
||||
newFieldValue?.type === 'inline_query' ? newFieldValue?.query : ''
|
||||
);
|
||||
|
||||
if (oldQuery !== newQuery) {
|
||||
changes.push({
|
||||
subfieldName: 'query',
|
||||
oldSubfieldValue: oldQuery,
|
||||
newSubfieldValue: newQuery,
|
||||
});
|
||||
}
|
||||
|
||||
const oldLanguage = stringifyToSortedJson(
|
||||
oldFieldValue?.type === 'inline_query' ? oldFieldValue?.language : ''
|
||||
);
|
||||
const newLanguage = stringifyToSortedJson(
|
||||
newFieldValue?.type === 'inline_query' ? newFieldValue?.language : ''
|
||||
);
|
||||
|
||||
if (oldLanguage !== newLanguage) {
|
||||
changes.push({
|
||||
subfieldName: 'language',
|
||||
oldSubfieldValue: oldLanguage,
|
||||
newSubfieldValue: newLanguage,
|
||||
});
|
||||
}
|
||||
|
||||
const oldFilters = stringifyToSortedJson(
|
||||
oldFieldValue?.type === 'inline_query' ? oldFieldValue?.filters : ''
|
||||
);
|
||||
const newFilters = stringifyToSortedJson(
|
||||
newFieldValue?.type === 'inline_query' ? newFieldValue?.filters : ''
|
||||
);
|
||||
|
||||
if (oldFilters !== newFilters) {
|
||||
changes.push({
|
||||
subfieldName: 'filters',
|
||||
oldSubfieldValue: oldFilters,
|
||||
newSubfieldValue: newFilters,
|
||||
});
|
||||
}
|
||||
|
||||
const oldSavedQueryId = stringifyToSortedJson(
|
||||
oldFieldValue?.type === 'saved_query' ? oldFieldValue?.saved_query_id : ''
|
||||
);
|
||||
const newSavedQueryId = stringifyToSortedJson(
|
||||
newFieldValue?.type === 'saved_query' ? newFieldValue?.saved_query_id : ''
|
||||
);
|
||||
|
||||
if (oldSavedQueryId !== newSavedQueryId) {
|
||||
changes.push({
|
||||
subfieldName: 'saved_query_id',
|
||||
oldSubfieldValue: oldSavedQueryId,
|
||||
newSubfieldValue: newSavedQueryId,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForRuleNameOverride = (
|
||||
oldFieldValue?: DiffableAllFields['rule_name_override'],
|
||||
newFieldValue?: DiffableAllFields['rule_name_override']
|
||||
): SubfieldChange[] => {
|
||||
const oldFieldName = stringifyToSortedJson(oldFieldValue?.field_name);
|
||||
const newFieldName = stringifyToSortedJson(newFieldValue?.field_name);
|
||||
|
||||
if (oldFieldName !== newFieldName) {
|
||||
return [
|
||||
{
|
||||
subfieldName: 'field_name',
|
||||
oldSubfieldValue: oldFieldName,
|
||||
newSubfieldValue: newFieldName,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForRuleSchedule = (
|
||||
oldFieldValue?: DiffableAllFields['rule_schedule'],
|
||||
newFieldValue?: DiffableAllFields['rule_schedule']
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
if (oldFieldValue?.interval !== newFieldValue?.interval) {
|
||||
changes.push({
|
||||
subfieldName: 'interval',
|
||||
oldSubfieldValue: stringifyToSortedJson(oldFieldValue?.interval),
|
||||
newSubfieldValue: stringifyToSortedJson(newFieldValue?.interval),
|
||||
});
|
||||
}
|
||||
|
||||
if (oldFieldValue?.lookback !== newFieldValue?.lookback) {
|
||||
changes.push({
|
||||
subfieldName: 'lookback',
|
||||
oldSubfieldValue: stringifyToSortedJson(oldFieldValue?.lookback),
|
||||
newSubfieldValue: stringifyToSortedJson(newFieldValue?.lookback),
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForThreatQuery = (
|
||||
oldFieldValue?: DiffableAllFields['threat_query'],
|
||||
newFieldValue?: DiffableAllFields['threat_query']
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldQuery = stringifyToSortedJson(oldFieldValue?.query);
|
||||
const newQuery = stringifyToSortedJson(newFieldValue?.query);
|
||||
|
||||
if (oldQuery !== newQuery) {
|
||||
changes.push({
|
||||
subfieldName: 'query',
|
||||
oldSubfieldValue: oldQuery,
|
||||
newSubfieldValue: newQuery,
|
||||
});
|
||||
}
|
||||
|
||||
const oldLanguage = stringifyToSortedJson(oldFieldValue?.language);
|
||||
const newLanguage = stringifyToSortedJson(newFieldValue?.language);
|
||||
|
||||
if (oldLanguage !== newLanguage) {
|
||||
changes.push({
|
||||
subfieldName: 'language',
|
||||
oldSubfieldValue: oldLanguage,
|
||||
newSubfieldValue: newLanguage,
|
||||
});
|
||||
}
|
||||
|
||||
const oldFilters = stringifyToSortedJson(oldFieldValue?.filters);
|
||||
const newFilters = stringifyToSortedJson(newFieldValue?.filters);
|
||||
|
||||
if (oldFilters !== newFilters) {
|
||||
changes.push({
|
||||
subfieldName: 'filters',
|
||||
oldSubfieldValue: oldFilters,
|
||||
newSubfieldValue: newFilters,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForThreshold = (
|
||||
oldFieldValue?: DiffableAllFields['threshold'],
|
||||
newFieldValue?: DiffableAllFields['threshold']
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldField = stringifyToSortedJson(oldFieldValue?.field);
|
||||
const newField = stringifyToSortedJson(newFieldValue?.field);
|
||||
|
||||
if (oldField !== newField) {
|
||||
changes.push({
|
||||
subfieldName: 'field',
|
||||
oldSubfieldValue: oldField,
|
||||
newSubfieldValue: newField,
|
||||
});
|
||||
}
|
||||
|
||||
const oldValue = stringifyToSortedJson(oldFieldValue?.value);
|
||||
const newValue = stringifyToSortedJson(newFieldValue?.value);
|
||||
|
||||
if (oldValue !== newValue) {
|
||||
changes.push({
|
||||
subfieldName: 'value',
|
||||
oldSubfieldValue: oldValue,
|
||||
newSubfieldValue: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
const oldCardinality = stringifyToSortedJson(oldFieldValue?.cardinality);
|
||||
const newCardinality = stringifyToSortedJson(newFieldValue?.cardinality);
|
||||
|
||||
if (oldCardinality !== newCardinality) {
|
||||
changes.push({
|
||||
subfieldName: 'cardinality',
|
||||
oldSubfieldValue: oldCardinality,
|
||||
newSubfieldValue: newCardinality,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForTimelineTemplate = (
|
||||
oldFieldValue?: DiffableAllFields['timeline_template'],
|
||||
newFieldValue?: DiffableAllFields['timeline_template']
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldTimelineId = stringifyToSortedJson(oldFieldValue?.timeline_id);
|
||||
const newTimelineId = stringifyToSortedJson(newFieldValue?.timeline_id);
|
||||
|
||||
if (oldTimelineId !== newTimelineId) {
|
||||
changes.push({
|
||||
subfieldName: 'timeline_id',
|
||||
oldSubfieldValue: oldTimelineId,
|
||||
newSubfieldValue: newTimelineId,
|
||||
});
|
||||
}
|
||||
|
||||
const oldTimelineTitle = stringifyToSortedJson(oldFieldValue?.timeline_title);
|
||||
const newTimelineTitle = stringifyToSortedJson(newFieldValue?.timeline_title);
|
||||
|
||||
if (oldTimelineTitle !== newTimelineTitle) {
|
||||
changes.push({
|
||||
subfieldName: 'timeline_title',
|
||||
oldSubfieldValue: oldTimelineTitle,
|
||||
newSubfieldValue: newTimelineTitle,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { stringifyToSortedJson } from '../utils';
|
||||
import type { DiffableAllFields } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { SubfieldChange } from '../types';
|
||||
|
||||
export const getSubfieldChangesForTimestampOverride = (
|
||||
oldFieldValue?: DiffableAllFields['timestamp_override'],
|
||||
newFieldValue?: DiffableAllFields['timestamp_override']
|
||||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldFieldName = stringifyToSortedJson(oldFieldValue?.field_name);
|
||||
const newFieldName = stringifyToSortedJson(newFieldValue?.field_name);
|
||||
|
||||
if (oldFieldName !== newFieldName) {
|
||||
changes.push({
|
||||
subfieldName: 'field_name',
|
||||
oldSubfieldValue: oldFieldName,
|
||||
newSubfieldValue: newFieldName,
|
||||
});
|
||||
}
|
||||
|
||||
const oldVersionFallbackDisabled = stringifyToSortedJson(oldFieldValue?.fallback_disabled);
|
||||
const newVersionFallbackDisabled = stringifyToSortedJson(newFieldValue?.fallback_disabled);
|
||||
|
||||
if (oldVersionFallbackDisabled !== newVersionFallbackDisabled) {
|
||||
changes.push({
|
||||
subfieldName: 'fallback_disabled',
|
||||
oldSubfieldValue: oldVersionFallbackDisabled,
|
||||
newSubfieldValue: newVersionFallbackDisabled,
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { EuiText } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export function NoChanges() {
|
||||
return (
|
||||
<EuiText size="s" color="subdued">
|
||||
{i18n.NO_CHANGES}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { DiffableAllFields } from '../../../../../../../common/api/detection_engine';
|
||||
import { DiffView } from '../../json_diff/diff_view';
|
||||
import { SubfieldHeader } from './subfield_header';
|
||||
import { FIELDS_WITH_SUBFIELDS } from './constants';
|
||||
|
||||
function shouldDisplaySubfieldLabelForField(fieldName: keyof DiffableAllFields): boolean {
|
||||
return FIELDS_WITH_SUBFIELDS.includes(fieldName);
|
||||
}
|
||||
|
||||
interface SubfieldProps {
|
||||
fieldName: keyof DiffableAllFields;
|
||||
subfieldName: string;
|
||||
oldSubfieldValue: string;
|
||||
newSubfieldValue: string;
|
||||
}
|
||||
|
||||
export const Subfield = ({
|
||||
fieldName,
|
||||
subfieldName,
|
||||
oldSubfieldValue,
|
||||
newSubfieldValue,
|
||||
}: SubfieldProps) => (
|
||||
<>
|
||||
{shouldDisplaySubfieldLabelForField(fieldName) && (
|
||||
<SubfieldHeader subfieldName={subfieldName} />
|
||||
)}
|
||||
<DiffView oldSource={oldSubfieldValue} newSource={newSubfieldValue} viewType="unified" />
|
||||
</>
|
||||
);
|
|
@ -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 React from 'react';
|
||||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
import type { SubfieldChanges } from './types';
|
||||
import { Subfield } from './subfield';
|
||||
import type { DiffableAllFields } from '../../../../../../../common/api/detection_engine';
|
||||
import { NoChanges } from './no_changes';
|
||||
|
||||
interface SubfieldChangesProps {
|
||||
fieldName: keyof DiffableAllFields;
|
||||
subfieldChanges: SubfieldChanges;
|
||||
}
|
||||
|
||||
export function SubfieldChanges({ fieldName, subfieldChanges }: SubfieldChangesProps) {
|
||||
if (subfieldChanges.length === 0) {
|
||||
return <NoChanges />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{subfieldChanges.map((change, index) => {
|
||||
const shouldShowSeparator = index !== subfieldChanges.length - 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subfield
|
||||
key={change.subfieldName}
|
||||
fieldName={fieldName}
|
||||
subfieldName={change.subfieldName}
|
||||
oldSubfieldValue={change.oldSubfieldValue}
|
||||
newSubfieldValue={change.newSubfieldValue}
|
||||
/>
|
||||
{shouldShowSeparator ? <EuiHorizontalRule margin="s" size="full" /> : null}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { camelCase, startCase } from 'lodash';
|
||||
import { EuiTitle, EuiSpacer } from '@elastic/eui';
|
||||
import { fieldToDisplayNameMap } from '../../diff_components/translations';
|
||||
|
||||
interface SubfieldHeaderProps {
|
||||
subfieldName: string;
|
||||
}
|
||||
|
||||
export function SubfieldHeader({ subfieldName }: SubfieldHeaderProps) {
|
||||
const subfieldLabel = fieldToDisplayNameMap[subfieldName] ?? startCase(camelCase(subfieldName));
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xxxs">
|
||||
<h4>{subfieldLabel}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const NO_CHANGES = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.upgradeRules.comparisonSide.noChangesLabel',
|
||||
{
|
||||
defaultMessage: 'No changes',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface SubfieldChange {
|
||||
readonly subfieldName: string;
|
||||
readonly oldSubfieldValue: string;
|
||||
readonly newSubfieldValue: string;
|
||||
}
|
||||
|
||||
export type SubfieldChanges = Readonly<SubfieldChange[]>;
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 stringify from 'json-stable-stringify';
|
||||
import { Version } from '../versions_picker/constants';
|
||||
import type {
|
||||
DiffableAllFields,
|
||||
ThreeWayDiff,
|
||||
} from '../../../../../../../common/api/detection_engine';
|
||||
|
||||
/**
|
||||
* Picks the field value for a given version either from a three-way diff object or from a user-set resolved value.
|
||||
*
|
||||
* @param version - The version for which the field value is to be picked.
|
||||
* @param fieldThreeWayDiff - The three-way diff object containing the field values for different versions.
|
||||
* @param resolvedValue - The user-set resolved value resolved value. Used if it is set and the version is final.
|
||||
* @returns - The field value for the specified version
|
||||
*/
|
||||
export function pickFieldValueForVersion<FieldName extends keyof DiffableAllFields>(
|
||||
version: Version,
|
||||
fieldThreeWayDiff: ThreeWayDiff<DiffableAllFields[FieldName]>,
|
||||
resolvedValue?: DiffableAllFields[FieldName]
|
||||
): DiffableAllFields[FieldName] | undefined {
|
||||
if (version === Version.Final) {
|
||||
return resolvedValue ?? fieldThreeWayDiff.merged_version;
|
||||
}
|
||||
|
||||
const versionFieldToPick = `${version}_version` as const;
|
||||
return fieldThreeWayDiff[versionFieldToPick];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringifies a field value to an alphabetically sorted JSON string.
|
||||
*/
|
||||
export const stringifyToSortedJson = (fieldValue: unknown): string => {
|
||||
if (fieldValue === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof fieldValue === 'string') {
|
||||
return fieldValue;
|
||||
}
|
||||
|
||||
return stringify(fieldValue, { space: 2 });
|
||||
};
|
|
@ -8,6 +8,13 @@
|
|||
import type { EuiSelectOption } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export enum Version {
|
||||
Base = 'base',
|
||||
Current = 'current',
|
||||
Target = 'target',
|
||||
Final = 'final',
|
||||
}
|
||||
|
||||
export enum SelectedVersions {
|
||||
BaseTarget = 'base_target',
|
||||
BaseCurrent = 'base_current',
|
||||
|
|
|
@ -26464,10 +26464,10 @@ react-colorful@^5.1.2:
|
|||
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
|
||||
integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==
|
||||
|
||||
react-diff-view@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-diff-view/-/react-diff-view-3.2.0.tgz#8fbf04782d78423903a59202ce7533f6312c1cc3"
|
||||
integrity sha512-p58XoqMxgt71ujpiDQTs9Za3nqTawt1E4bTzKsYSqr8I8br6cjQj1b66HxGnV8Yrc6MD6iQPqS1aZiFoGqEw+g==
|
||||
react-diff-view@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-diff-view/-/react-diff-view-3.2.1.tgz#cc1473955fae999c1d486638c4425ceb48f3d465"
|
||||
integrity sha512-JoDahgiyeReeH9W9lrI3Z4c4esbd/HNAOdThj6Pce/ZAukFBmXSbZ4Qv8ayo7yow+fTpRNfqtQ9gX5nArEi08w==
|
||||
dependencies:
|
||||
classnames "^2.3.2"
|
||||
diff-match-patch "^1.0.5"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue