mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] Extend upgrade prebuilt rules context with conflict resolution functionality (#191721)
**Addresses:** https://github.com/elastic/kibana/issues/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.
This commit is contained in:
parent
bc8fc413c4
commit
66af356731
30 changed files with 601 additions and 368 deletions
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -109,7 +108,7 @@ export const convertRuleToDiffable = (rule: RuleResponse | PrebuiltRuleAsset): D
|
|||
};
|
||||
|
||||
const extractDiffableCommonFields = (
|
||||
rule: RuleResponse | PrebuiltRuleAsset
|
||||
rule: RuleResponse
|
||||
): RequiredOptional<DiffableCommonFields> => {
|
||||
return {
|
||||
// --------------------- REQUIRED FIELDS
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
|
@ -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,
|
|
@ -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;
|
|
@ -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';
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
||||
});
|
|
@ -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)}</>;
|
||||
}
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { invariant } from '../../../../../common/utils/invariant';
|
||||
import type { RuleObjectId } from '../../../../../common/api/detection_engine';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
|
||||
export interface RuleDetailsFlyoutState {
|
||||
previewedRule: RuleResponse | null;
|
||||
}
|
||||
|
||||
export interface RuleDetailsFlyoutActions {
|
||||
openRulePreview: (ruleId: RuleObjectId) => void;
|
||||
closeRulePreview: () => void;
|
||||
}
|
||||
|
||||
export const useRuleDetailsFlyout = (
|
||||
rules: RuleResponse[]
|
||||
): RuleDetailsFlyoutState & RuleDetailsFlyoutActions => {
|
||||
const [previewedRule, setRuleForPreview] = React.useState<RuleResponse | null>(null);
|
||||
|
||||
const openRulePreview = useCallback(
|
||||
(ruleId: RuleObjectId) => {
|
||||
const ruleToShowInFlyout = rules.find((rule) => {
|
||||
return rule.id === ruleId;
|
||||
});
|
||||
invariant(ruleToShowInFlyout, `Rule with id ${ruleId} not found`);
|
||||
setRuleForPreview(ruleToShowInFlyout);
|
||||
},
|
||||
[rules, setRuleForPreview]
|
||||
);
|
||||
|
||||
const closeRulePreview = useCallback(() => {
|
||||
setRuleForPreview(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
openRulePreview,
|
||||
closeRulePreview,
|
||||
previewedRule,
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
|
||||
|
@ -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}
|
||||
|
|
|
@ -32,8 +32,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
|
|||
const {
|
||||
state: {
|
||||
rules,
|
||||
filteredRules,
|
||||
isFetched,
|
||||
hasRulesToInstall,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
selectedRules,
|
||||
|
@ -43,8 +42,6 @@ export const AddPrebuiltRulesTable = React.memo(() => {
|
|||
} = addRulesTableContext;
|
||||
const rulesColumns = useAddPrebuiltRulesTableColumns();
|
||||
|
||||
const isTableEmpty = isFetched && rules.length === 0;
|
||||
|
||||
const shouldShowProgress = isUpgradingSecurityPackages || isRefetching;
|
||||
|
||||
return (
|
||||
|
@ -66,7 +63,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
|
|||
</>
|
||||
}
|
||||
loadedContent={
|
||||
isTableEmpty ? (
|
||||
!hasRulesToInstall ? (
|
||||
<AddPrebuiltRulesTableNoItemsMessage />
|
||||
) : (
|
||||
<>
|
||||
|
@ -80,7 +77,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
|
|||
</EuiFlexGroup>
|
||||
|
||||
<EuiInMemoryTable
|
||||
items={filteredRules}
|
||||
items={rules}
|
||||
sorting
|
||||
pagination={{
|
||||
initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE,
|
||||
|
|
|
@ -20,21 +20,16 @@ import {
|
|||
import { usePrebuiltRulesInstallReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review';
|
||||
import type { AddPrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_install';
|
||||
import { useFilterPrebuiltRulesToInstall } from './use_filter_prebuilt_rules_to_install';
|
||||
import { useRuleDetailsFlyout } from '../../../../rule_management/components/rule_details/use_rule_details_flyout';
|
||||
import { useRulePreviewFlyout } from '../use_rule_preview_flyout';
|
||||
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout';
|
||||
import * as i18n from './translations';
|
||||
import { isUpgradeReviewRequestEnabled } from './add_prebuilt_rules_utils';
|
||||
|
||||
export interface AddPrebuiltRulesTableState {
|
||||
/**
|
||||
* Rules available to be installed
|
||||
* Rules available to be installed after applying `filterOptions`
|
||||
*/
|
||||
rules: RuleResponse[];
|
||||
/**
|
||||
* Rules to display in table after applying filters
|
||||
*/
|
||||
filteredRules: RuleResponse[];
|
||||
/**
|
||||
* Currently selected table filter
|
||||
*/
|
||||
|
@ -43,6 +38,10 @@ export interface AddPrebuiltRulesTableState {
|
|||
* All unique tags for all rules
|
||||
*/
|
||||
tags: string[];
|
||||
/**
|
||||
* Indicates whether there are rules (without filters applied) available to install.
|
||||
*/
|
||||
hasRulesToInstall: boolean;
|
||||
/**
|
||||
* Is true then there is no cached data and the query is currently fetching.
|
||||
*/
|
||||
|
@ -95,6 +94,8 @@ interface AddPrebuiltRulesTableContextProviderProps {
|
|||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PREBUILT_RULE_INSTALL_FLYOUT_ANCHOR = 'installPrebuiltRulePreview';
|
||||
|
||||
export const AddPrebuiltRulesTableContextProvider = ({
|
||||
children,
|
||||
}: AddPrebuiltRulesTableContextProviderProps) => {
|
||||
|
@ -138,15 +139,6 @@ export const AddPrebuiltRulesTableContextProvider = ({
|
|||
|
||||
const filteredRules = useFilterPrebuiltRulesToInstall({ filterOptions, rules });
|
||||
|
||||
const { openRulePreview, closeRulePreview, previewedRule } = useRuleDetailsFlyout(filteredRules);
|
||||
|
||||
const isPreviewRuleLoading =
|
||||
previewedRule?.rule_id && loadingRules.includes(previewedRule.rule_id);
|
||||
const canPreviewedRuleBeInstalled =
|
||||
!userInfoLoading &&
|
||||
canUserCRUD &&
|
||||
!(isPreviewRuleLoading || isRefetching || isUpgradingSecurityPackages);
|
||||
|
||||
const installOneRule = useCallback(
|
||||
async (ruleId: RuleSignatureId) => {
|
||||
const rule = rules.find((r) => r.rule_id === ruleId);
|
||||
|
@ -187,6 +179,47 @@ export const AddPrebuiltRulesTableContextProvider = ({
|
|||
}
|
||||
}, [installAllRulesRequest, rules]);
|
||||
|
||||
const ruleActionsFactory = useCallback(
|
||||
(rule: RuleResponse, closeRulePreview: () => void) => {
|
||||
const isPreviewRuleLoading = loadingRules.includes(rule.rule_id);
|
||||
const canPreviewedRuleBeInstalled =
|
||||
!userInfoLoading &&
|
||||
canUserCRUD &&
|
||||
!(isPreviewRuleLoading || isRefetching || isUpgradingSecurityPackages);
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
disabled={!canPreviewedRuleBeInstalled}
|
||||
onClick={() => {
|
||||
installOneRule(rule.rule_id);
|
||||
closeRulePreview();
|
||||
}}
|
||||
fill
|
||||
data-test-subj="installPrebuiltRuleFromFlyoutButton"
|
||||
>
|
||||
{i18n.INSTALL_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
);
|
||||
},
|
||||
[
|
||||
loadingRules,
|
||||
userInfoLoading,
|
||||
canUserCRUD,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
installOneRule,
|
||||
]
|
||||
);
|
||||
|
||||
const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
|
||||
rules: filteredRules,
|
||||
ruleActionsFactory,
|
||||
flyoutProps: {
|
||||
id: PREBUILT_RULE_INSTALL_FLYOUT_ANCHOR,
|
||||
dataTestSubj: PREBUILT_RULE_INSTALL_FLYOUT_ANCHOR,
|
||||
},
|
||||
});
|
||||
|
||||
const actions = useMemo(
|
||||
() => ({
|
||||
setFilterOptions,
|
||||
|
@ -203,10 +236,10 @@ export const AddPrebuiltRulesTableContextProvider = ({
|
|||
const providerValue = useMemo<AddPrebuiltRulesContextType>(() => {
|
||||
return {
|
||||
state: {
|
||||
rules,
|
||||
filteredRules,
|
||||
rules: filteredRules,
|
||||
filterOptions,
|
||||
tags,
|
||||
hasRulesToInstall: isFetched && rules.length > 0,
|
||||
isFetched,
|
||||
isLoading,
|
||||
loadingRules,
|
||||
|
@ -236,26 +269,7 @@ export const AddPrebuiltRulesTableContextProvider = ({
|
|||
<AddPrebuiltRulesTableContext.Provider value={providerValue}>
|
||||
<>
|
||||
{children}
|
||||
{previewedRule && (
|
||||
<RuleDetailsFlyout
|
||||
rule={previewedRule}
|
||||
dataTestSubj="installPrebuiltRulePreview"
|
||||
closeFlyout={closeRulePreview}
|
||||
ruleActions={
|
||||
<EuiButton
|
||||
disabled={!canPreviewedRuleBeInstalled}
|
||||
onClick={() => {
|
||||
installOneRule(previewedRule.rule_id ?? '');
|
||||
closeRulePreview();
|
||||
}}
|
||||
fill
|
||||
data-test-subj="installPrebuiltRuleFromFlyoutButton"
|
||||
>
|
||||
{i18n.INSTALL_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{rulePreviewFlyout}
|
||||
</>
|
||||
</AddPrebuiltRulesTableContext.Provider>
|
||||
);
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
EuiSkeletonText,
|
||||
EuiSkeletonTitle,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations';
|
||||
import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants';
|
||||
import { RulesChangelogLink } from '../rules_changelog_link';
|
||||
|
@ -23,6 +23,7 @@ import { UpgradePrebuiltRulesTableButtons } from './upgrade_prebuilt_rules_table
|
|||
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
|
||||
import { UpgradePrebuiltRulesTableFilters } from './upgrade_prebuilt_rules_table_filters';
|
||||
import { useUpgradePrebuiltRulesTableColumns } from './use_upgrade_prebuilt_rules_table_columns';
|
||||
import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state';
|
||||
|
||||
const NO_ITEMS_MESSAGE = (
|
||||
<EuiEmptyPrompt
|
||||
|
@ -38,23 +39,22 @@ const NO_ITEMS_MESSAGE = (
|
|||
*/
|
||||
export const UpgradePrebuiltRulesTable = React.memo(() => {
|
||||
const upgradeRulesTableContext = useUpgradePrebuiltRulesTableContext();
|
||||
const [selected, setSelected] = useState<RuleUpgradeState[]>([]);
|
||||
|
||||
const {
|
||||
state: {
|
||||
rules,
|
||||
filteredRules,
|
||||
isFetched,
|
||||
rulesUpgradeState,
|
||||
hasRulesToUpgrade,
|
||||
isLoading,
|
||||
selectedRules,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
},
|
||||
actions: { selectRules },
|
||||
} = upgradeRulesTableContext;
|
||||
const ruleUpgradeStatesArray = useMemo(
|
||||
() => Object.values(rulesUpgradeState),
|
||||
[rulesUpgradeState]
|
||||
);
|
||||
const rulesColumns = useUpgradePrebuiltRulesTableColumns();
|
||||
|
||||
const isTableEmpty = isFetched && rules.length === 0;
|
||||
|
||||
const shouldShowProgress = isUpgradingSecurityPackages || isRefetching;
|
||||
|
||||
return (
|
||||
|
@ -76,7 +76,7 @@ export const UpgradePrebuiltRulesTable = React.memo(() => {
|
|||
</>
|
||||
}
|
||||
loadedContent={
|
||||
isTableEmpty ? (
|
||||
!hasRulesToUpgrade ? (
|
||||
NO_ITEMS_MESSAGE
|
||||
) : (
|
||||
<>
|
||||
|
@ -95,14 +95,14 @@ export const UpgradePrebuiltRulesTable = React.memo(() => {
|
|||
<UpgradePrebuiltRulesTableFilters />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UpgradePrebuiltRulesTableButtons />
|
||||
<UpgradePrebuiltRulesTableButtons selectedRules={selected} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiInMemoryTable
|
||||
items={filteredRules}
|
||||
items={ruleUpgradeStatesArray}
|
||||
sorting
|
||||
pagination={{
|
||||
initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE,
|
||||
|
@ -110,8 +110,8 @@ export const UpgradePrebuiltRulesTable = React.memo(() => {
|
|||
}}
|
||||
selection={{
|
||||
selectable: () => true,
|
||||
onSelectionChange: selectRules,
|
||||
initialSelected: selectedRules,
|
||||
onSelectionChange: setSelected,
|
||||
initialSelected: selected,
|
||||
}}
|
||||
itemId="rule_id"
|
||||
data-test-subj="rules-upgrades-table"
|
||||
|
|
|
@ -6,25 +6,35 @@
|
|||
*/
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useUserData } from '../../../../../detections/components/user_info';
|
||||
import * as i18n from './translations';
|
||||
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
|
||||
import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state';
|
||||
|
||||
export const UpgradePrebuiltRulesTableButtons = () => {
|
||||
interface UpgradePrebuiltRulesTableButtonsProps {
|
||||
selectedRules: RuleUpgradeState[];
|
||||
}
|
||||
|
||||
export const UpgradePrebuiltRulesTableButtons = ({
|
||||
selectedRules,
|
||||
}: UpgradePrebuiltRulesTableButtonsProps) => {
|
||||
const {
|
||||
state: { rules, selectedRules, loadingRules, isRefetching, isUpgradingSecurityPackages },
|
||||
actions: { upgradeAllRules, upgradeSelectedRules },
|
||||
state: { hasRulesToUpgrade, loadingRules, isRefetching, isUpgradingSecurityPackages },
|
||||
actions: { upgradeAllRules, upgradeRules },
|
||||
} = useUpgradePrebuiltRulesTableContext();
|
||||
const [{ loading: isUserDataLoading, canUserCRUD }] = useUserData();
|
||||
const canUserEditRules = canUserCRUD && !isUserDataLoading;
|
||||
|
||||
const isRulesAvailableForUpgrade = rules.length > 0;
|
||||
const numberOfSelectedRules = selectedRules.length ?? 0;
|
||||
const shouldDisplayUpgradeSelectedRulesButton = numberOfSelectedRules > 0;
|
||||
|
||||
const isRuleUpgrading = loadingRules.length > 0;
|
||||
const isRequestInProgress = isRuleUpgrading || isRefetching || isUpgradingSecurityPackages;
|
||||
const upgradeSelectedRules = useCallback(
|
||||
() => upgradeRules(selectedRules.map((rule) => rule.rule_id)),
|
||||
[selectedRules, upgradeRules]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
|
||||
|
@ -47,7 +57,7 @@ export const UpgradePrebuiltRulesTableButtons = () => {
|
|||
fill
|
||||
iconType="plusInCircle"
|
||||
onClick={upgradeAllRules}
|
||||
disabled={!canUserEditRules || !isRulesAvailableForUpgrade || isRequestInProgress}
|
||||
disabled={!canUserEditRules || !hasRulesToUpgrade || isRequestInProgress}
|
||||
data-test-subj="upgradeAllRulesButton"
|
||||
>
|
||||
{i18n.UPDATE_ALL}
|
||||
|
|
|
@ -8,14 +8,17 @@
|
|||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { EuiButton, EuiToolTip } from '@elastic/eui';
|
||||
import type { EuiTabbedContentTab } from '@elastic/eui';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { ThreeWayDiffTab } from '../../../../rule_management/components/rule_details/three_way_diff_tab';
|
||||
import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab';
|
||||
import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
|
||||
import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs';
|
||||
import { useBoolState } from '../../../../../common/hooks/use_bool_state';
|
||||
import { affectedJobIds } from '../../../../../detections/components/callouts/ml_job_compatibility_callout/affected_job_ids';
|
||||
import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules';
|
||||
import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import type {
|
||||
RuleResponse,
|
||||
RuleSignatureId,
|
||||
} from '../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { invariant } from '../../../../../../common/utils/invariant';
|
||||
import {
|
||||
usePerformUpgradeAllRules,
|
||||
|
@ -25,25 +28,20 @@ import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic
|
|||
import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_upgrade';
|
||||
import { useFilterPrebuiltRulesToUpgrade } from './use_filter_prebuilt_rules_to_upgrade';
|
||||
import { useAsyncConfirmation } from '../rules_table/use_async_confirmation';
|
||||
import { useRuleDetailsFlyout } from '../../../../rule_management/components/rule_details/use_rule_details_flyout';
|
||||
import {
|
||||
RuleDetailsFlyout,
|
||||
TabContentPadding,
|
||||
} from '../../../../rule_management/components/rule_details/rule_details_flyout';
|
||||
import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout';
|
||||
import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab';
|
||||
import { MlJobUpgradeModal } from '../../../../../detections/components/modals/ml_job_upgrade_modal';
|
||||
import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations';
|
||||
import * as i18n from './translations';
|
||||
import type { RulesUpgradeState } from './use_prebuilt_rules_upgrade_state';
|
||||
import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state';
|
||||
import { useRulePreviewFlyout } from '../use_rule_preview_flyout';
|
||||
|
||||
export interface UpgradePrebuiltRulesTableState {
|
||||
/**
|
||||
* Rules available to be updated
|
||||
* Rule upgrade state after applying `filterOptions`
|
||||
*/
|
||||
rules: RuleUpgradeInfoForReview[];
|
||||
/**
|
||||
* Rules to display in table after applying filters
|
||||
*/
|
||||
filteredRules: RuleUpgradeInfoForReview[];
|
||||
rulesUpgradeState: RulesUpgradeState;
|
||||
/**
|
||||
* Currently selected table filter
|
||||
*/
|
||||
|
@ -52,6 +50,10 @@ export interface UpgradePrebuiltRulesTableState {
|
|||
* All unique tags for all rules
|
||||
*/
|
||||
tags: string[];
|
||||
/**
|
||||
* Indicates whether there are rules (without filters applied) to upgrade.
|
||||
*/
|
||||
hasRulesToUpgrade: boolean;
|
||||
/**
|
||||
* Is true then there is no cached data and the query is currently fetching.
|
||||
*/
|
||||
|
@ -78,21 +80,15 @@ export interface UpgradePrebuiltRulesTableState {
|
|||
* The timestamp for when the rules were successfully fetched
|
||||
*/
|
||||
lastUpdated: number;
|
||||
/**
|
||||
* Rule rows selected in EUI InMemory Table
|
||||
*/
|
||||
selectedRules: RuleUpgradeInfoForReview[];
|
||||
}
|
||||
|
||||
export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview';
|
||||
|
||||
export interface UpgradePrebuiltRulesTableActions {
|
||||
reFetchRules: () => void;
|
||||
upgradeOneRule: (ruleId: string) => void;
|
||||
upgradeSelectedRules: () => void;
|
||||
upgradeRules: (ruleIds: RuleSignatureId[]) => void;
|
||||
upgradeAllRules: () => void;
|
||||
setFilterOptions: Dispatch<SetStateAction<UpgradePrebuiltRulesTableFilterOptions>>;
|
||||
selectRules: (rules: RuleUpgradeInfoForReview[]) => void;
|
||||
openRulePreview: (ruleId: string) => void;
|
||||
}
|
||||
|
||||
|
@ -112,8 +108,10 @@ interface UpgradePrebuiltRulesTableContextProviderProps {
|
|||
export const UpgradePrebuiltRulesTableContextProvider = ({
|
||||
children,
|
||||
}: UpgradePrebuiltRulesTableContextProviderProps) => {
|
||||
const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled(
|
||||
'prebuiltRulesCustomizationEnabled'
|
||||
);
|
||||
const [loadingRules, setLoadingRules] = useState<RuleSignatureId[]>([]);
|
||||
const [selectedRules, setSelectedRules] = useState<RuleUpgradeInfoForReview[]>([]);
|
||||
const [filterOptions, setFilterOptions] = useState<UpgradePrebuiltRulesTableFilterOptions>({
|
||||
filter: '',
|
||||
tags: [],
|
||||
|
@ -122,7 +120,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
|
||||
|
||||
const {
|
||||
data: { rules, stats: { tags } } = {
|
||||
data: { rules: ruleUpgradeInfos, stats: { tags } } = {
|
||||
rules: [],
|
||||
stats: { tags: [] },
|
||||
},
|
||||
|
@ -135,20 +133,12 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
refetchInterval: false, // Disable automatic refetching since request is expensive
|
||||
keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change
|
||||
});
|
||||
|
||||
const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules();
|
||||
const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules();
|
||||
|
||||
const filteredRules = useFilterPrebuiltRulesToUpgrade({ filterOptions, rules });
|
||||
|
||||
const { openRulePreview, closeRulePreview, previewedRule } = useRuleDetailsFlyout(
|
||||
filteredRules.map((upgradeInfo) => upgradeInfo.target_rule)
|
||||
);
|
||||
const canPreviewedRuleBeUpgraded = Boolean(
|
||||
(previewedRule?.rule_id && loadingRules.includes(previewedRule.rule_id)) ||
|
||||
isRefetching ||
|
||||
isUpgradingSecurityPackages
|
||||
);
|
||||
const filteredRuleUpgradeInfos = useFilterPrebuiltRulesToUpgrade({
|
||||
filterOptions,
|
||||
rules: ruleUpgradeInfos,
|
||||
});
|
||||
const { rulesUpgradeState, setFieldResolvedValue } =
|
||||
usePrebuiltRulesUpgradeState(filteredRuleUpgradeInfos);
|
||||
|
||||
// Wrapper to add confirmation modal for users who may be running older ML Jobs that would
|
||||
// be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121
|
||||
|
@ -163,51 +153,36 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
|
||||
const shouldConfirmUpgrade = legacyJobsInstalled.length > 0;
|
||||
|
||||
const upgradeOneRule = useCallback(
|
||||
async (ruleId: RuleSignatureId) => {
|
||||
const rule = rules.find((r) => r.rule_id === ruleId);
|
||||
invariant(rule, `Rule with id ${ruleId} not found`);
|
||||
const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules();
|
||||
const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules();
|
||||
|
||||
setLoadingRules((prev) => [...prev, ruleId]);
|
||||
const upgradeRules = useCallback(
|
||||
async (ruleIds: RuleSignatureId[]) => {
|
||||
const rulesToUpgrade = ruleIds.map((ruleId) => ({
|
||||
rule_id: ruleId,
|
||||
version:
|
||||
rulesUpgradeState[ruleId].diff.fields.version?.target_version ??
|
||||
rulesUpgradeState[ruleId].current_rule.version,
|
||||
revision: rulesUpgradeState[ruleId].revision,
|
||||
}));
|
||||
setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]);
|
||||
try {
|
||||
if (shouldConfirmUpgrade && !(await confirmUpgrade())) {
|
||||
return;
|
||||
}
|
||||
await upgradeSpecificRulesRequest([
|
||||
{
|
||||
rule_id: ruleId,
|
||||
version: rule.diff.fields.version?.target_version ?? rule.current_rule.version,
|
||||
revision: rule.revision,
|
||||
},
|
||||
]);
|
||||
await upgradeSpecificRulesRequest(rulesToUpgrade);
|
||||
} finally {
|
||||
setLoadingRules((prev) => prev.filter((id) => id !== ruleId));
|
||||
setLoadingRules((prev) =>
|
||||
prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id))
|
||||
);
|
||||
}
|
||||
},
|
||||
[confirmUpgrade, rules, shouldConfirmUpgrade, upgradeSpecificRulesRequest]
|
||||
[confirmUpgrade, shouldConfirmUpgrade, rulesUpgradeState, upgradeSpecificRulesRequest]
|
||||
);
|
||||
|
||||
const upgradeSelectedRules = useCallback(async () => {
|
||||
const rulesToUpgrade = selectedRules.map((rule) => ({
|
||||
rule_id: rule.rule_id,
|
||||
version: rule.diff.fields.version?.target_version ?? rule.current_rule.version,
|
||||
revision: rule.revision,
|
||||
}));
|
||||
setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]);
|
||||
try {
|
||||
if (shouldConfirmUpgrade && !(await confirmUpgrade())) {
|
||||
return;
|
||||
}
|
||||
await upgradeSpecificRulesRequest(rulesToUpgrade);
|
||||
} finally {
|
||||
setLoadingRules((prev) => prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id)));
|
||||
setSelectedRules([]);
|
||||
}
|
||||
}, [confirmUpgrade, selectedRules, shouldConfirmUpgrade, upgradeSpecificRulesRequest]);
|
||||
|
||||
const upgradeAllRules = useCallback(async () => {
|
||||
// Unselect all rules so that the table doesn't show the "bulk actions" bar
|
||||
setLoadingRules((prev) => [...prev, ...rules.map((r) => r.rule_id)]);
|
||||
setLoadingRules((prev) => [...prev, ...ruleUpgradeInfos.map((r) => r.rule_id)]);
|
||||
try {
|
||||
if (shouldConfirmUpgrade && !(await confirmUpgrade())) {
|
||||
return;
|
||||
|
@ -215,43 +190,137 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
await upgradeAllRulesRequest();
|
||||
} finally {
|
||||
setLoadingRules([]);
|
||||
setSelectedRules([]);
|
||||
}
|
||||
}, [confirmUpgrade, rules, shouldConfirmUpgrade, upgradeAllRulesRequest]);
|
||||
}, [confirmUpgrade, ruleUpgradeInfos, shouldConfirmUpgrade, upgradeAllRulesRequest]);
|
||||
|
||||
const ruleActionsFactory = useCallback(
|
||||
(rule: RuleResponse, closeRulePreview: () => void) => (
|
||||
<EuiButton
|
||||
disabled={
|
||||
loadingRules.includes(rule.rule_id) ||
|
||||
isRefetching ||
|
||||
isUpgradingSecurityPackages ||
|
||||
rulesUpgradeState[rule.rule_id]?.hasUnresolvedConflicts
|
||||
}
|
||||
onClick={() => {
|
||||
upgradeRules([rule.rule_id]);
|
||||
closeRulePreview();
|
||||
}}
|
||||
fill
|
||||
data-test-subj="updatePrebuiltRuleFromFlyoutButton"
|
||||
>
|
||||
{i18n.UPDATE_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
),
|
||||
[rulesUpgradeState, loadingRules, isRefetching, isUpgradingSecurityPackages, upgradeRules]
|
||||
);
|
||||
const extraTabsFactory = useCallback(
|
||||
(rule: RuleResponse) => {
|
||||
const ruleUpgradeState = rulesUpgradeState[rule.rule_id];
|
||||
|
||||
if (!ruleUpgradeState) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const extraTabs = [
|
||||
{
|
||||
id: 'updates',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_PER_FIELD_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.UPDATES_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: (
|
||||
<TabContentPadding>
|
||||
<PerFieldRuleDiffTab ruleDiff={ruleUpgradeState.diff} />
|
||||
</TabContentPadding>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'jsonViewUpdates',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_JSON_VIEW_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: (
|
||||
<TabContentPadding>
|
||||
<RuleDiffTab
|
||||
oldRule={ruleUpgradeState.current_rule}
|
||||
newRule={ruleUpgradeState.target_rule}
|
||||
/>
|
||||
</TabContentPadding>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (isPrebuiltRulesCustomizationEnabled) {
|
||||
extraTabs.unshift({
|
||||
id: 'diff',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_PER_FIELD_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.DIFF_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: (
|
||||
<TabContentPadding>
|
||||
<ThreeWayDiffTab
|
||||
finalDiffableRule={ruleUpgradeState.finalRule}
|
||||
setFieldResolvedValue={setFieldResolvedValue}
|
||||
/>
|
||||
</TabContentPadding>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return extraTabs;
|
||||
},
|
||||
[rulesUpgradeState, setFieldResolvedValue, isPrebuiltRulesCustomizationEnabled]
|
||||
);
|
||||
const filteredRules = useMemo(
|
||||
() => filteredRuleUpgradeInfos.map((rule) => rule.target_rule),
|
||||
[filteredRuleUpgradeInfos]
|
||||
);
|
||||
const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
|
||||
rules: filteredRules,
|
||||
ruleActionsFactory,
|
||||
extraTabsFactory,
|
||||
flyoutProps: {
|
||||
id: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR,
|
||||
dataTestSubj: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR,
|
||||
},
|
||||
});
|
||||
|
||||
const actions = useMemo<UpgradePrebuiltRulesTableActions>(
|
||||
() => ({
|
||||
reFetchRules: refetch,
|
||||
upgradeOneRule,
|
||||
upgradeSelectedRules,
|
||||
upgradeRules,
|
||||
upgradeAllRules,
|
||||
setFilterOptions,
|
||||
selectRules: setSelectedRules,
|
||||
openRulePreview,
|
||||
}),
|
||||
[refetch, upgradeOneRule, upgradeSelectedRules, upgradeAllRules, openRulePreview]
|
||||
[refetch, upgradeRules, upgradeAllRules, openRulePreview]
|
||||
);
|
||||
|
||||
const providerValue = useMemo<UpgradePrebuiltRulesContextType>(() => {
|
||||
return {
|
||||
state: {
|
||||
rules,
|
||||
filteredRules,
|
||||
rulesUpgradeState,
|
||||
hasRulesToUpgrade: isFetched && ruleUpgradeInfos.length > 0,
|
||||
filterOptions,
|
||||
tags,
|
||||
isFetched,
|
||||
isLoading: isLoading && loadingJobs,
|
||||
isLoading: isLoading || loadingJobs,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
selectedRules,
|
||||
loadingRules,
|
||||
lastUpdated: dataUpdatedAt,
|
||||
},
|
||||
actions,
|
||||
};
|
||||
}, [
|
||||
rules,
|
||||
filteredRules,
|
||||
ruleUpgradeInfos,
|
||||
rulesUpgradeState,
|
||||
filterOptions,
|
||||
tags,
|
||||
isFetched,
|
||||
|
@ -259,49 +328,11 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
loadingJobs,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
selectedRules,
|
||||
loadingRules,
|
||||
dataUpdatedAt,
|
||||
actions,
|
||||
]);
|
||||
|
||||
const extraTabs = useMemo<EuiTabbedContentTab[]>(() => {
|
||||
const activeRule = previewedRule && filteredRules.find(({ id }) => id === previewedRule.id);
|
||||
|
||||
if (!activeRule) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'updates',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_PER_FIELD_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.UPDATES_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: (
|
||||
<TabContentPadding>
|
||||
<PerFieldRuleDiffTab ruleDiff={activeRule.diff} />
|
||||
</TabContentPadding>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'jsonViewUpdates',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_JSON_VIEW_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: (
|
||||
<TabContentPadding>
|
||||
<RuleDiffTab oldRule={activeRule.current_rule} newRule={activeRule.target_rule} />
|
||||
</TabContentPadding>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [previewedRule, filteredRules]);
|
||||
|
||||
return (
|
||||
<UpgradePrebuiltRulesTableContext.Provider value={providerValue}>
|
||||
<>
|
||||
|
@ -313,29 +344,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
/>
|
||||
)}
|
||||
{children}
|
||||
{previewedRule && (
|
||||
<RuleDetailsFlyout
|
||||
rule={previewedRule}
|
||||
size="l"
|
||||
id={PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR}
|
||||
dataTestSubj="updatePrebuiltRulePreview"
|
||||
closeFlyout={closeRulePreview}
|
||||
ruleActions={
|
||||
<EuiButton
|
||||
disabled={canPreviewedRuleBeUpgraded}
|
||||
onClick={() => {
|
||||
upgradeOneRule(previewedRule.rule_id ?? '');
|
||||
closeRulePreview();
|
||||
}}
|
||||
fill
|
||||
data-test-subj="updatePrebuiltRuleFromFlyoutButton"
|
||||
>
|
||||
{i18n.UPDATE_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
}
|
||||
extraTabs={extraTabs}
|
||||
/>
|
||||
)}
|
||||
{rulePreviewFlyout}
|
||||
</>
|
||||
</UpgradePrebuiltRulesTableContext.Provider>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo, useState } from 'react';
|
||||
import type { FieldsDiff } from '../../../../../../common/api/detection_engine';
|
||||
import {
|
||||
ThreeWayDiffConflict,
|
||||
type DiffableAllFields,
|
||||
type DiffableRule,
|
||||
type RuleObjectId,
|
||||
type RuleSignatureId,
|
||||
type RuleUpgradeInfoForReview,
|
||||
} from '../../../../../../common/api/detection_engine';
|
||||
import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable';
|
||||
|
||||
export interface RuleUpgradeState extends RuleUpgradeInfoForReview {
|
||||
/**
|
||||
* Rule containing desired values users expect to see in the upgraded rule.
|
||||
*/
|
||||
finalRule: DiffableRule;
|
||||
/**
|
||||
* Indicates whether there are conflicts blocking rule upgrading.
|
||||
*/
|
||||
hasUnresolvedConflicts: boolean;
|
||||
}
|
||||
export type RulesUpgradeState = Record<RuleSignatureId, RuleUpgradeState>;
|
||||
export type SetFieldResolvedValueFn<
|
||||
FieldName extends keyof DiffableAllFields = keyof DiffableAllFields
|
||||
> = (params: {
|
||||
ruleId: RuleObjectId;
|
||||
fieldName: FieldName;
|
||||
resolvedValue: DiffableAllFields[FieldName];
|
||||
}) => void;
|
||||
|
||||
type RuleResolvedConflicts = Partial<DiffableAllFields>;
|
||||
type RulesResolvedConflicts = Record<string, RuleResolvedConflicts>;
|
||||
|
||||
interface UseRulesUpgradeStateResult {
|
||||
rulesUpgradeState: RulesUpgradeState;
|
||||
setFieldResolvedValue: SetFieldResolvedValueFn;
|
||||
}
|
||||
|
||||
export function usePrebuiltRulesUpgradeState(
|
||||
ruleUpgradeInfos: RuleUpgradeInfoForReview[]
|
||||
): UseRulesUpgradeStateResult {
|
||||
const [rulesResolvedConflicts, setRulesResolvedConflicts] = useState<RulesResolvedConflicts>({});
|
||||
const setFieldResolvedValue = useCallback((...[params]: Parameters<SetFieldResolvedValueFn>) => {
|
||||
setRulesResolvedConflicts((prevRulesResolvedConflicts) => ({
|
||||
...prevRulesResolvedConflicts,
|
||||
[params.ruleId]: {
|
||||
...(prevRulesResolvedConflicts[params.ruleId] ?? {}),
|
||||
[params.fieldName]: params.resolvedValue,
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
const rulesUpgradeState = useMemo(() => {
|
||||
const state: RulesUpgradeState = {};
|
||||
|
||||
for (const ruleUpgradeInfo of ruleUpgradeInfos) {
|
||||
state[ruleUpgradeInfo.rule_id] = {
|
||||
...ruleUpgradeInfo,
|
||||
finalRule: calcFinalDiffableRule(
|
||||
ruleUpgradeInfo,
|
||||
rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {}
|
||||
),
|
||||
hasUnresolvedConflicts:
|
||||
getUnacceptedConflictsCount(
|
||||
ruleUpgradeInfo.diff.fields,
|
||||
rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {}
|
||||
) > 0,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}, [ruleUpgradeInfos, rulesResolvedConflicts]);
|
||||
|
||||
return {
|
||||
rulesUpgradeState,
|
||||
setFieldResolvedValue,
|
||||
};
|
||||
}
|
||||
|
||||
function calcFinalDiffableRule(
|
||||
ruleUpgradeInfo: RuleUpgradeInfoForReview,
|
||||
ruleResolvedConflicts: RuleResolvedConflicts
|
||||
): DiffableRule {
|
||||
return {
|
||||
...convertRuleToDiffable(ruleUpgradeInfo.target_rule),
|
||||
...convertRuleFieldsDiffToDiffable(ruleUpgradeInfo.diff.fields),
|
||||
...ruleResolvedConflicts,
|
||||
} as DiffableRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles a `DiffableRule` from rule fields diff `merge_value`s.
|
||||
*/
|
||||
function convertRuleFieldsDiffToDiffable(
|
||||
ruleFieldsDiff: FieldsDiff<Record<string, unknown>>
|
||||
): Partial<DiffableRule> {
|
||||
const mergeVersionRule: Record<string, unknown> = {};
|
||||
|
||||
for (const fieldName of Object.keys(ruleFieldsDiff)) {
|
||||
mergeVersionRule[fieldName] = ruleFieldsDiff[fieldName].merged_version;
|
||||
}
|
||||
|
||||
return mergeVersionRule;
|
||||
}
|
||||
|
||||
function getUnacceptedConflictsCount(
|
||||
ruleFieldsDiff: FieldsDiff<Record<string, unknown>>,
|
||||
ruleResolvedConflicts: RuleResolvedConflicts
|
||||
): number {
|
||||
const fieldNames = Object.keys(ruleFieldsDiff);
|
||||
const fieldNamesWithConflict = fieldNames.filter(
|
||||
(fieldName) => ruleFieldsDiff[fieldName].conflict !== ThreeWayDiffConflict.NONE
|
||||
);
|
||||
const fieldNamesWithConflictSet = new Set(fieldNamesWithConflict);
|
||||
|
||||
for (const resolvedConflictField of Object.keys(ruleResolvedConflicts)) {
|
||||
if (fieldNamesWithConflictSet.has(resolvedConflictField)) {
|
||||
fieldNamesWithConflictSet.delete(resolvedConflictField);
|
||||
}
|
||||
}
|
||||
|
||||
return fieldNamesWithConflictSet.size;
|
||||
}
|
|
@ -10,7 +10,6 @@ import { EuiBadge, EuiButtonEmpty, EuiLink, EuiLoadingSpinner, EuiText } from '@
|
|||
import React, { useMemo } from 'react';
|
||||
import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name';
|
||||
import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants';
|
||||
import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules';
|
||||
import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { PopoverItems } from '../../../../../common/components/popover_items';
|
||||
import { useUiSetting$ } from '../../../../../common/lib/kibana';
|
||||
|
@ -23,8 +22,9 @@ import type { Rule } from '../../../../rule_management/logic';
|
|||
import { getNormalizedSeverity } from '../helpers';
|
||||
import type { UpgradePrebuiltRulesTableActions } from './upgrade_prebuilt_rules_table_context';
|
||||
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
|
||||
import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state';
|
||||
|
||||
export type TableColumn = EuiBasicTableColumn<RuleUpgradeInfoForReview>;
|
||||
export type TableColumn = EuiBasicTableColumn<RuleUpgradeState>;
|
||||
|
||||
interface RuleNameProps {
|
||||
name: string;
|
||||
|
@ -51,10 +51,9 @@ const RuleName = ({ name, ruleId }: RuleNameProps) => {
|
|||
const RULE_NAME_COLUMN: TableColumn = {
|
||||
field: 'current_rule.name',
|
||||
name: i18n.COLUMN_RULE,
|
||||
render: (
|
||||
value: RuleUpgradeInfoForReview['current_rule']['name'],
|
||||
rule: RuleUpgradeInfoForReview
|
||||
) => <RuleName name={value} ruleId={rule.id} />,
|
||||
render: (value: RuleUpgradeState['current_rule']['name'], ruleUpgradeState: RuleUpgradeState) => (
|
||||
<RuleName name={value} ruleId={ruleUpgradeState.id} />
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '60%',
|
||||
|
@ -106,30 +105,30 @@ const INTEGRATIONS_COLUMN: TableColumn = {
|
|||
};
|
||||
|
||||
const createUpgradeButtonColumn = (
|
||||
upgradeOneRule: UpgradePrebuiltRulesTableActions['upgradeOneRule'],
|
||||
upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'],
|
||||
loadingRules: RuleSignatureId[],
|
||||
isDisabled: boolean
|
||||
): TableColumn => ({
|
||||
field: 'rule_id',
|
||||
name: <RulesTableEmptyColumnName name={i18n.UPDATE_RULE_BUTTON} />,
|
||||
render: (ruleId: RuleUpgradeInfoForReview['rule_id']) => {
|
||||
render: (ruleId: RuleSignatureId, record) => {
|
||||
const isRuleUpgrading = loadingRules.includes(ruleId);
|
||||
const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled;
|
||||
const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled || record.hasUnresolvedConflicts;
|
||||
const spinner = (
|
||||
<EuiLoadingSpinner
|
||||
size="s"
|
||||
data-test-subj={`upgradeSinglePrebuiltRuleButton-loadingSpinner-${ruleId}`}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
disabled={isUpgradeButtonDisabled}
|
||||
onClick={() => upgradeOneRule(ruleId)}
|
||||
onClick={() => upgradeRules([ruleId])}
|
||||
data-test-subj={`upgradeSinglePrebuiltRuleButton-${ruleId}`}
|
||||
>
|
||||
{isRuleUpgrading ? (
|
||||
<EuiLoadingSpinner
|
||||
size="s"
|
||||
data-test-subj={`upgradeSinglePrebuiltRuleButton-loadingSpinner-${ruleId}`}
|
||||
/>
|
||||
) : (
|
||||
i18n.UPDATE_RULE_BUTTON
|
||||
)}
|
||||
{isRuleUpgrading ? spinner : i18n.UPDATE_RULE_BUTTON}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
},
|
||||
|
@ -143,9 +142,8 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
|
|||
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
|
||||
const {
|
||||
state: { loadingRules, isRefetching, isUpgradingSecurityPackages },
|
||||
actions: { upgradeOneRule },
|
||||
actions: { upgradeRules },
|
||||
} = useUpgradePrebuiltRulesTableContext();
|
||||
|
||||
const isDisabled = isRefetching || isUpgradingSecurityPackages;
|
||||
|
||||
return useMemo(
|
||||
|
@ -169,15 +167,15 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
|
|||
field: 'current_rule.severity',
|
||||
name: i18n.COLUMN_SEVERITY,
|
||||
render: (value: Rule['severity']) => <SeverityBadge value={value} />,
|
||||
sortable: ({ current_rule: { severity } }: RuleUpgradeInfoForReview) =>
|
||||
sortable: ({ current_rule: { severity } }: RuleUpgradeState) =>
|
||||
getNormalizedSeverity(severity),
|
||||
truncateText: true,
|
||||
width: '12%',
|
||||
},
|
||||
...(hasCRUDPermissions
|
||||
? [createUpgradeButtonColumn(upgradeOneRule, loadingRules, isDisabled)]
|
||||
? [createUpgradeButtonColumn(upgradeRules, loadingRules, isDisabled)]
|
||||
: []),
|
||||
],
|
||||
[hasCRUDPermissions, loadingRules, isDisabled, showRelatedIntegrations, upgradeOneRule]
|
||||
[hasCRUDPermissions, loadingRules, isDisabled, showRelatedIntegrations, upgradeRules]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { ReactNode } from 'react';
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import type { EuiTabbedContentTab } from '@elastic/eui';
|
||||
import { invariant } from '../../../../../common/utils/invariant';
|
||||
import type { RuleObjectId } from '../../../../../common/api/detection_engine';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { RuleDetailsFlyout } from '../../../rule_management/components/rule_details/rule_details_flyout';
|
||||
|
||||
interface UseRulePreviewFlyoutParams {
|
||||
rules: RuleResponse[];
|
||||
ruleActionsFactory: (rule: RuleResponse, closeRulePreview: () => void) => ReactNode;
|
||||
extraTabsFactory?: (rule: RuleResponse) => EuiTabbedContentTab[];
|
||||
flyoutProps: RulePreviewFlyoutProps;
|
||||
}
|
||||
|
||||
interface RulePreviewFlyoutProps {
|
||||
/**
|
||||
* Rule preview flyout unique id used in HTML
|
||||
*/
|
||||
id: string;
|
||||
dataTestSubj: string;
|
||||
}
|
||||
|
||||
interface UseRulePreviewFlyoutResult {
|
||||
rulePreviewFlyout: ReactNode;
|
||||
openRulePreview: (ruleId: RuleObjectId) => void;
|
||||
closeRulePreview: () => void;
|
||||
}
|
||||
|
||||
export function useRulePreviewFlyout({
|
||||
rules,
|
||||
extraTabsFactory,
|
||||
ruleActionsFactory,
|
||||
flyoutProps,
|
||||
}: UseRulePreviewFlyoutParams): UseRulePreviewFlyoutResult {
|
||||
const [rule, setRuleForPreview] = useState<RuleResponse | undefined>();
|
||||
const closeRulePreview = useCallback(() => setRuleForPreview(undefined), []);
|
||||
const ruleActions = useMemo(
|
||||
() => rule && ruleActionsFactory(rule, closeRulePreview),
|
||||
[rule, ruleActionsFactory, closeRulePreview]
|
||||
);
|
||||
const extraTabs = useMemo(
|
||||
() => (rule && extraTabsFactory ? extraTabsFactory(rule) : []),
|
||||
[rule, extraTabsFactory]
|
||||
);
|
||||
|
||||
return {
|
||||
rulePreviewFlyout: rule && (
|
||||
<RuleDetailsFlyout
|
||||
rule={rule}
|
||||
size="l"
|
||||
id={flyoutProps.id}
|
||||
dataTestSubj={flyoutProps.dataTestSubj}
|
||||
closeFlyout={closeRulePreview}
|
||||
ruleActions={ruleActions}
|
||||
extraTabs={extraTabs}
|
||||
/>
|
||||
),
|
||||
openRulePreview: useCallback(
|
||||
(ruleId: RuleObjectId) => {
|
||||
const ruleToShowInFlyout = rules.find((x) => x.id === ruleId);
|
||||
|
||||
invariant(ruleToShowInFlyout, `Rule with id ${ruleId} not found`);
|
||||
setRuleForPreview(ruleToShowInFlyout);
|
||||
},
|
||||
[rules, setRuleForPreview]
|
||||
),
|
||||
closeRulePreview,
|
||||
};
|
||||
}
|
|
@ -18,9 +18,10 @@ import {
|
|||
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { invariant } from '../../../../../../common/utils/invariant';
|
||||
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
|
||||
import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable';
|
||||
import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response';
|
||||
|
||||
import { calculateRuleFieldsDiff } from './calculation/calculate_rule_fields_diff';
|
||||
import { convertRuleToDiffable } from './normalization/convert_rule_to_diffable';
|
||||
|
||||
export interface RuleVersions {
|
||||
current?: RuleResponse;
|
||||
|
@ -65,13 +66,19 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult =
|
|||
const { base, current, target } = args;
|
||||
|
||||
invariant(current != null, 'current version is required');
|
||||
const diffableCurrentVersion = convertRuleToDiffable(current);
|
||||
const diffableCurrentVersion = convertRuleToDiffable(
|
||||
convertPrebuiltRuleAssetToRuleResponse(current)
|
||||
);
|
||||
|
||||
invariant(target != null, 'target version is required');
|
||||
const diffableTargetVersion = convertRuleToDiffable(target);
|
||||
const diffableTargetVersion = convertRuleToDiffable(
|
||||
convertPrebuiltRuleAssetToRuleResponse(target)
|
||||
);
|
||||
|
||||
// Base version is optional
|
||||
const diffableBaseVersion = base ? convertRuleToDiffable(base) : undefined;
|
||||
const diffableBaseVersion = base
|
||||
? convertRuleToDiffable(convertPrebuiltRuleAssetToRuleResponse(base))
|
||||
: undefined;
|
||||
|
||||
const fieldsDiff = calculateRuleFieldsDiff({
|
||||
base_version: diffableBaseVersion || MissingVersion,
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import type { BuildingBlockObject } from '../../../../../../../common/api/detection_engine/prebuilt_rules';
|
||||
import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset';
|
||||
|
||||
export const extractBuildingBlockObject = (
|
||||
rule: RuleResponse | PrebuiltRuleAsset
|
||||
): BuildingBlockObject | undefined => {
|
||||
if (rule.building_block_type == null) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
type: rule.building_block_type,
|
||||
};
|
||||
};
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { addEcsToRequiredFields } from '../../../../../../../common/detection_engine/rule_management/utils';
|
||||
import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { addEcsToRequiredFields } from '../../../utils/utils';
|
||||
import type { PrebuiltRuleAsset } from '../../../../prebuilt_rules';
|
||||
import { RULE_DEFAULTS } from '../mergers/apply_rule_defaults';
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rul
|
|||
import type { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import type { RuleActionCamel } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
|
||||
import { addEcsToRequiredFields } from '../../../../../../../common/detection_engine/rule_management/utils';
|
||||
import type {
|
||||
RuleResponse,
|
||||
TypeSpecificCreateProps,
|
||||
|
@ -25,7 +26,7 @@ import { assertUnreachable } from '../../../../../../../common/utility_types';
|
|||
import { convertObjectKeysToCamelCase } from '../../../../../../utils/object_case_converters';
|
||||
import type { RuleParams, TypeSpecificRuleParams } from '../../../../rule_schema';
|
||||
import { transformToActionFrequency } from '../../../normalization/rule_actions';
|
||||
import { addEcsToRequiredFields, separateActionsAndSystemAction } from '../../../utils/utils';
|
||||
import { separateActionsAndSystemAction } from '../../../utils/utils';
|
||||
|
||||
/**
|
||||
* These are the fields that are added to the rule response that are not part of the rule params
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { addEcsToRequiredFields } from '../../../../../../../common/detection_engine/rule_management/utils';
|
||||
import type {
|
||||
RuleCreateProps,
|
||||
RuleSource,
|
||||
|
@ -20,7 +21,6 @@ import {
|
|||
normalizeThresholdObject,
|
||||
} from '../../../../../../../common/detection_engine/utils';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
import { addEcsToRequiredFields } from '../../../utils/utils';
|
||||
|
||||
export const RULE_DEFAULTS = {
|
||||
enabled: false,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { BadRequestError } from '@kbn/securitysolution-es-utils';
|
||||
import { stringifyZodError } from '@kbn/zod-helpers';
|
||||
import { addEcsToRequiredFields } from '../../../../../../../common/detection_engine/rule_management/utils';
|
||||
import type {
|
||||
EqlRule,
|
||||
EqlRuleResponseFields,
|
||||
|
@ -44,7 +45,6 @@ import {
|
|||
} from '../../../../../../../common/detection_engine/utils';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
|
||||
import { addEcsToRequiredFields } from '../../../utils/utils';
|
||||
import { calculateRuleSource } from './rule_source/calculate_rule_source';
|
||||
|
||||
interface ApplyRulePatchProps {
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { RuleResponse } from '../../../../../../../../common/api/detection_
|
|||
import { MissingVersion } from '../../../../../../../../common/api/detection_engine';
|
||||
import type { PrebuiltRuleAsset } from '../../../../../prebuilt_rules';
|
||||
import { calculateRuleFieldsDiff } from '../../../../../prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff';
|
||||
import { convertRuleToDiffable } from '../../../../../prebuilt_rules/logic/diff/normalization/convert_rule_to_diffable';
|
||||
import { convertRuleToDiffable } from '../../../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable';
|
||||
import { convertPrebuiltRuleAssetToRuleResponse } from '../../converters/convert_prebuilt_rule_asset_to_rule_response';
|
||||
|
||||
export function calculateIsCustomized(
|
||||
|
|
|
@ -9,8 +9,6 @@ import { partition, isEmpty } from 'lodash/fp';
|
|||
import pMap from 'p-map';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ecsFieldMap } from '@kbn/alerts-as-data-utils';
|
||||
|
||||
import type { ActionsClient, FindActionResult } from '@kbn/actions-plugin/server';
|
||||
import type { FindResult, PartialRule } from '@kbn/alerting-plugin/server';
|
||||
import type { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
@ -18,8 +16,6 @@ import type { RuleAction } from '@kbn/securitysolution-io-ts-alerting-types';
|
|||
|
||||
import type {
|
||||
InvestigationFields,
|
||||
RequiredField,
|
||||
RequiredFieldInput,
|
||||
RuleResponse,
|
||||
RuleAction as RuleActionSchema,
|
||||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
|
@ -375,22 +371,6 @@ export const migrateLegacyInvestigationFields = (
|
|||
return investigationFields;
|
||||
};
|
||||
|
||||
/*
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
export const separateActionsAndSystemAction = (
|
||||
actionsClient: ActionsClient,
|
||||
actions: RuleActionSchema[] | undefined
|
||||
|
|
|
@ -145,15 +145,24 @@ export const bulkCreateRuleAssets = ({
|
|||
const url = `${Cypress.env('ELASTICSEARCH_URL')}/${index}/_bulk?refresh`;
|
||||
|
||||
const bulkIndexRequestBody = rules.reduce((body, rule) => {
|
||||
const indexOperation = {
|
||||
const document = JSON.stringify(rule);
|
||||
const documentId = `security-rule:${rule['security-rule'].rule_id}`;
|
||||
const historicalDocumentId = `${documentId}_${rule['security-rule'].version}`;
|
||||
|
||||
const indexRuleAsset = `${JSON.stringify({
|
||||
index: {
|
||||
_index: index,
|
||||
_id: `security-rule:${rule['security-rule'].rule_id}`,
|
||||
_id: documentId,
|
||||
},
|
||||
};
|
||||
})}\n${document}\n`;
|
||||
const indexHistoricalRuleAsset = `${JSON.stringify({
|
||||
index: {
|
||||
_index: index,
|
||||
_id: historicalDocumentId,
|
||||
},
|
||||
})}\n${document}\n`;
|
||||
|
||||
const documentData = JSON.stringify(rule);
|
||||
return body.concat(JSON.stringify(indexOperation), '\n', documentData, '\n');
|
||||
return body.concat(indexRuleAsset, indexHistoricalRuleAsset);
|
||||
}, '');
|
||||
|
||||
rootRequest({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue