Skip to content

Commit

Permalink
[Security Solution] Rule Diff Phase 2 components (elastic#174564)
Browse files Browse the repository at this point in the history
## Summary

Addresses elastic#166489
Docs issue: elastic/security-docs#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

<img width="1587" alt="Screenshot 2024-02-07 at 1 36 34 AM"
src="https://github.com/elastic/kibana/assets/56367316/85dce529-064e-4025-b82c-2e89f6ec800b">
<img width="994" alt="Screenshot 2024-02-07 at 1 36 52 AM"
src="https://github.com/elastic/kibana/assets/56367316/c226973f-ad46-4565-90c0-437316b138b4">

### 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 <[email protected]>
  • Loading branch information
2 people authored and CoenWarmer committed Feb 15, 2024
1 parent d273ca4 commit e8ddd22
Show file tree
Hide file tree
Showing 21 changed files with 1,187 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof DiffableRule>;
export const DiffableRule = t.intersection([
DiffableCommonFields,
Expand All @@ -262,6 +263,7 @@ export type DiffableAllFields = DiffableCommonFields &
Omit<DiffableCustomQueryFields, 'type'> &
Omit<DiffableSavedQueryFields, 'type'> &
Omit<DiffableEqlFields, 'type'> &
Omit<DiffableEsqlFields, 'type'> &
Omit<DiffableThreatMatchFields, 'type'> &
Omit<DiffableThresholdFields, 'type'> &
Omit<DiffableMachineLearningFields, 'type'> &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,26 @@ export interface PartialRuleDiff {
fields: Partial<RuleFieldsDiff>;
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;
15 changes: 15 additions & 0 deletions x-pack/plugins/security_solution/common/experimental_features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,28 @@ 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,
/*
* Disables discover esql tab within timeline
*
*/
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<keyof ExperimentalFeatures>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<keyof DiffableAllFields> = [
'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<keyof DiffableAllFields> = [
'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<keyof DiffableAllFields> = ['rule_schedule'];

export const SETUP_UPGRADE_FIELD_ORDER: Array<keyof DiffableAllFields> = ['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<keyof DiffableAllFields> = [
// 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,
];
Original file line number Diff line number Diff line change
@@ -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;
}) => (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexGroup direction="column">
{shouldShowSubtitles ? (
<EuiTitle size="xxxs">
<h4>{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}</h4>
</EuiTitle>
) : null}
<DiffView oldSource={currentVersion} newSource={targetVersion} />
{shouldShowSeparator ? <EuiHorizontalRule margin="s" size="full" /> : null}
</EuiFlexGroup>
</EuiFlexGroup>
);

export interface FieldDiffComponentProps {
ruleDiffs: FormattedFieldDiff;
fieldsGroupName: string;
}

export const FieldGroupDiffComponent = ({
ruleDiffs,
fieldsGroupName,
}: FieldDiffComponentProps) => {
const { fieldDiffs, shouldShowSubtitles } = ruleDiffs;
return (
<RuleDiffPanelWrapper fieldName={fieldsGroupName}>
{fieldDiffs.map(({ currentVersion, targetVersion, fieldName: specificFieldName }, index) => {
const shouldShowSeparator = index !== fieldDiffs.length - 1;
return (
<SubFieldComponent
key={specificFieldName}
shouldShowSeparator={shouldShowSeparator}
shouldShowSubtitles={shouldShowSubtitles}
currentVersion={currentVersion}
targetVersion={targetVersion}
fieldName={specificFieldName}
/>
);
})}
</RuleDiffPanelWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<div
css={css`
position: sticky;
top: 0;
background: ${euiTheme.colors.emptyShade};
z-index: 1; // Fixes accordion button displaying above header bug
`}
>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexGroup alignItems="baseline" gutterSize="xs">
<EuiIconTip
color="subdued"
content={i18n.CURRENT_VERSION_DESCRIPTION}
type="iInCircle"
size="m"
display="block"
/>
<EuiTitle size="xxs">
<h6>{i18n.CURRENT_RULE_VERSION}</h6>
</EuiTitle>
</EuiFlexGroup>
<EuiFlexGroup alignItems="baseline" gutterSize="xs">
<EuiIconTip
color="subdued"
content={i18n.UPDATED_VERSION_DESCRIPTION}
type="iInCircle"
size="m"
/>
<EuiTitle size="xxs">
<h6>{i18n.ELASTIC_UPDATE_VERSION}</h6>
</EuiTitle>
</EuiFlexGroup>
</EuiFlexGroup>
<EuiHorizontalRule margin="s" size="full" />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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 (
<EuiSplitPanel.Outer hasBorder>
<EuiAccordion
initialIsOpen={true}
css={css`
.euiAccordion__triggerWrapper {
background: ${euiTheme.colors.lightestShade};
padding: ${euiTheme.size.m};
}
`}
id={fieldName}
buttonContent={
<EuiTitle size="xs">
<h5>{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}</h5>
</EuiTitle>
}
>
<EuiSplitPanel.Inner color="transparent">{children}</EuiSplitPanel.Inner>
</EuiAccordion>
</EuiSplitPanel.Outer>
);
};
Original file line number Diff line number Diff line change
@@ -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) => (
<>
<EuiSpacer size="m" />
<EuiAccordion
initialIsOpen={true}
id={title}
css={css`
padding-top: 1px; // Fixes border disappearing bug
`}
buttonContent={
<EuiTitle size="s">
<h3>{title}</h3>
</EuiTitle>
}
>
{fieldGroups.map(({ fieldsGroupName, formattedDiffs }) => {
return (
<React.Fragment key={fieldsGroupName}>
<EuiSpacer size="m" />
<FieldGroupDiffComponent ruleDiffs={formattedDiffs} fieldsGroupName={fieldsGroupName} />
</React.Fragment>
);
})}
</EuiAccordion>
</>
);
Loading

0 comments on commit e8ddd22

Please sign in to comment.