Skip to content

Commit

Permalink
[Security Solution] Extend upgrade prebuilt rules context with confli…
Browse files Browse the repository at this point in the history
…ct resolution functionality (#191721)

**Addresses:** #171520

## Summary

This PR implements necessary `UpgradePrebuiltRulesTableContext` changes to provide uses a way to resolve conflicts manually by providing field's resolved value.

## Details

During prebuilt rules upgrading users may encounter solvable and non-solvable conflicts between customized and target rule versions. Three-Way-Diff field component allow to specify a desired resolve value user expects to be in the rule after upgrading. It's also possible to customize rules during the upgrading process.

Current functionality is informational only without an ability to customize prebuilt rules. As the core part of that process it's required to manage the upgrading state and provide necessary data for downstream components rendering field diffs and accepting user input.

**This PR extends** `UpgradePrebuiltRulesTableContext` with rule upgrade state and provides it to `ThreeWayDiffTab` stub component. It's planned to add implementation to `ThreeWayDiffTab` in follow up PRs.

**On top of that** `UpgradePrebuiltRulesTableContext` and `AddPrebuiltRulesTableContext` were symmetrically refactored from architecture point of view to improve encapsulation by separation of concerns which leads to slight complexity reduction.

### Feature flag `prebuiltRulesCustomizationEnabled`

`ThreeWayDiffTab` is hidden under a feature flag `prebuiltRulesCustomizationEnabled`. It accepts a `finalDiffableRule` which represents rule fields the user expects to see in the upgraded rule. `finalDiffableRule`  is a combination of field resolved values and target rule fields where resolved values have precedence.
  • Loading branch information
maximpn authored Sep 10, 2024
1 parent bc8fc41 commit 66af356
Show file tree
Hide file tree
Showing 30 changed files with 601 additions and 368 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import type { RequiredOptional } from '@kbn/zod-helpers';
import { requiredOptional } from '@kbn/zod-helpers';
import { DEFAULT_MAX_SIGNALS } from '../../../../../../../common/constants';
import { assertUnreachable } from '../../../../../../../common/utility_types';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
import { assertUnreachable } from '../../../utility_types';
import type {
EqlRule,
EqlRuleCreateProps,
Expand All @@ -27,8 +27,7 @@ import type {
ThreatMatchRuleCreateProps,
ThresholdRule,
ThresholdRuleCreateProps,
} from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset';
} from '../../../api/detection_engine/model/rule_schema';
import type {
DiffableCommonFields,
DiffableCustomQueryFields,
Expand All @@ -40,7 +39,8 @@ import type {
DiffableSavedQueryFields,
DiffableThreatMatchFields,
DiffableThresholdFields,
} from '../../../../../../../common/api/detection_engine/prebuilt_rules';
} from '../../../api/detection_engine/prebuilt_rules';
import { addEcsToRequiredFields } from '../../rule_management/utils';
import { extractBuildingBlockObject } from './extract_building_block_object';
import {
extractInlineKqlQuery,
Expand All @@ -53,13 +53,12 @@ import { extractRuleNameOverrideObject } from './extract_rule_name_override_obje
import { extractRuleSchedule } from './extract_rule_schedule';
import { extractTimelineTemplateReference } from './extract_timeline_template_reference';
import { extractTimestampOverrideObject } from './extract_timestamp_override_object';
import { addEcsToRequiredFields } from '../../../../rule_management/utils/utils';

/**
* Normalizes a given rule to the form which is suitable for passing to the diff algorithm.
* Read more in the JSDoc description of DiffableRule.
*/
export const convertRuleToDiffable = (rule: RuleResponse | PrebuiltRuleAsset): DiffableRule => {
export const convertRuleToDiffable = (rule: RuleResponse): DiffableRule => {
const commonFields = extractDiffableCommonFields(rule);

switch (rule.type) {
Expand Down Expand Up @@ -109,7 +108,7 @@ export const convertRuleToDiffable = (rule: RuleResponse | PrebuiltRuleAsset): D
};

const extractDiffableCommonFields = (
rule: RuleResponse | PrebuiltRuleAsset
rule: RuleResponse
): RequiredOptional<DiffableCommonFields> => {
return {
// --------------------- REQUIRED FIELDS
Expand Down
Original file line number Diff line number Diff line change
@@ -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 type { RuleResponse } from '../../../api/detection_engine/model/rule_schema';
import type { BuildingBlockObject } from '../../../api/detection_engine/prebuilt_rules';

export const extractBuildingBlockObject = (rule: RuleResponse): BuildingBlockObject | undefined => {
if (rule.building_block_type == null) {
return undefined;
}
return {
type: rule.building_block_type,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import type {
KqlQueryLanguage,
RuleFilterArray,
RuleQuery,
} from '../../../../../../../common/api/detection_engine/model/rule_schema';
} from '../../../api/detection_engine/model/rule_schema';
import type {
InlineKqlQuery,
RuleEqlQuery,
RuleEsqlQuery,
RuleKqlQuery,
} from '../../../../../../../common/api/detection_engine/prebuilt_rules';
import { KqlQueryType } from '../../../../../../../common/api/detection_engine/prebuilt_rules';
} from '../../../api/detection_engine/prebuilt_rules';
import { KqlQueryType } from '../../../api/detection_engine/prebuilt_rules';

export const extractRuleKqlQuery = (
query: RuleQuery | undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import type {
DataViewId,
IndexPatternArray,
} from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { RuleDataSource } from '../../../../../../../common/api/detection_engine/prebuilt_rules';
import { DataSourceType } from '../../../../../../../common/api/detection_engine/prebuilt_rules';
} from '../../../api/detection_engine/model/rule_schema';
import type { RuleDataSource } from '../../../api/detection_engine/prebuilt_rules';
import { DataSourceType } from '../../../api/detection_engine/prebuilt_rules';

export const extractRuleDataSource = (
indexPatterns: IndexPatternArray | undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
* 2.0.
*/

import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { RuleNameOverrideObject } from '../../../../../../../common/api/detection_engine/prebuilt_rules';
import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset';
import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema';
import type { RuleNameOverrideObject } from '../../../api/detection_engine/prebuilt_rules';

export const extractRuleNameOverrideObject = (
rule: RuleResponse | PrebuiltRuleAsset
rule: RuleResponse
): RuleNameOverrideObject | undefined => {
if (rule.rule_name_override == null) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@ import moment from 'moment';
import dateMath from '@elastic/datemath';
import { parseDuration } from '@kbn/alerting-plugin/common';

import type {
RuleMetadata,
RuleResponse,
} from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { RuleSchedule } from '../../../../../../../common/api/detection_engine/prebuilt_rules';
import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset';
import type { RuleMetadata, RuleResponse } from '../../../api/detection_engine/model/rule_schema';
import type { RuleSchedule } from '../../../api/detection_engine/prebuilt_rules';

export const extractRuleSchedule = (rule: RuleResponse | PrebuiltRuleAsset): RuleSchedule => {
export const extractRuleSchedule = (rule: RuleResponse): RuleSchedule => {
const interval = rule.interval ?? '5m';
const from = rule.from ?? 'now-6m';
const to = rule.to ?? 'now';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
* 2.0.
*/

import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { TimelineTemplateReference } from '../../../../../../../common/api/detection_engine/prebuilt_rules';
import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset';
import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema';
import type { TimelineTemplateReference } from '../../../api/detection_engine/prebuilt_rules';

export const extractTimelineTemplateReference = (
rule: RuleResponse | PrebuiltRuleAsset
rule: RuleResponse
): TimelineTemplateReference | undefined => {
if (rule.timeline_id == null) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
* 2.0.
*/

import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { TimestampOverrideObject } from '../../../../../../../common/api/detection_engine/prebuilt_rules';
import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset';
import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema';
import type { TimestampOverrideObject } from '../../../api/detection_engine/prebuilt_rules';

export const extractTimestampOverrideObject = (
rule: RuleResponse | PrebuiltRuleAsset
rule: RuleResponse
): TimestampOverrideObject | undefined => {
if (rule.timestamp_override == null) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { ecsFieldMap } from '@kbn/alerts-as-data-utils';
import type { RequiredField, RequiredFieldInput } from '../../api/detection_engine';

/*
Computes the boolean "ecs" property value for each required field based on the ECS field map.
"ecs" property indicates whether the required field is an ECS field or not.
*/
export const addEcsToRequiredFields = (requiredFields?: RequiredFieldInput[]): RequiredField[] =>
(requiredFields ?? []).map((requiredFieldWithoutEcs) => {
const isEcsField = Boolean(
ecsFieldMap[requiredFieldWithoutEcs.name]?.type === requiredFieldWithoutEcs.type
);

return {
...requiredFieldWithoutEcs,
ecs: isEcsField,
};
});
Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import type { DiffableRule } from '../../../../../common/api/detection_engine';
import type { SetFieldResolvedValueFn } from '../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state';

interface ThreeWayDiffTabProps {
finalDiffableRule: DiffableRule;
setFieldResolvedValue: SetFieldResolvedValueFn;
}

export function ThreeWayDiffTab({
finalDiffableRule,
setFieldResolvedValue,
}: ThreeWayDiffTabProps): JSX.Element {
return <>{JSON.stringify(finalDiffableRule)}</>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export const UPDATES_TAB_LABEL = i18n.translate(
}
);

export const DIFF_TAB_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.diffTabLabel',
{
defaultMessage: 'Diff',
}
);

export const JSON_VIEW_UPDATES_TAB_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.jsonViewUpdatesTabLabel',
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import * as i18n from './translations';

export const AddPrebuiltRulesHeaderButtons = () => {
const {
state: { rules, selectedRules, loadingRules, isRefetching, isUpgradingSecurityPackages },
state: {
selectedRules,
loadingRules,
isRefetching,
isUpgradingSecurityPackages,
hasRulesToInstall,
},
actions: { installAllRules, installSelectedRules },
} = useAddPrebuiltRulesTableContext();
const [{ loading: isUserDataLoading, canUserCRUD }] = useUserData();
const canUserEditRules = canUserCRUD && !isUserDataLoading;

const isRulesAvailableForInstall = rules.length > 0;
const numberOfSelectedRules = selectedRules.length ?? 0;
const shouldDisplayInstallSelectedRulesButton = numberOfSelectedRules > 0;

Expand All @@ -46,7 +51,7 @@ export const AddPrebuiltRulesHeaderButtons = () => {
iconType="plusInCircle"
data-test-subj="installAllRulesButton"
onClick={installAllRules}
disabled={!canUserEditRules || !isRulesAvailableForInstall || isRequestInProgress}
disabled={!canUserEditRules || !hasRulesToInstall || isRequestInProgress}
aria-label={i18n.INSTALL_ALL_ARIA_LABEL}
>
{i18n.INSTALL_ALL}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
const {
state: {
rules,
filteredRules,
isFetched,
hasRulesToInstall,
isLoading,
isRefetching,
selectedRules,
Expand All @@ -43,8 +42,6 @@ export const AddPrebuiltRulesTable = React.memo(() => {
} = addRulesTableContext;
const rulesColumns = useAddPrebuiltRulesTableColumns();

const isTableEmpty = isFetched && rules.length === 0;

const shouldShowProgress = isUpgradingSecurityPackages || isRefetching;

return (
Expand All @@ -66,7 +63,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
</>
}
loadedContent={
isTableEmpty ? (
!hasRulesToInstall ? (
<AddPrebuiltRulesTableNoItemsMessage />
) : (
<>
Expand All @@ -80,7 +77,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
</EuiFlexGroup>

<EuiInMemoryTable
items={filteredRules}
items={rules}
sorting
pagination={{
initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE,
Expand Down
Loading

0 comments on commit 66af356

Please sign in to comment.