[Rules migration] Retry failed translations (#11383) (#204619)

## Summary

[Internal link](https://github.com/elastic/security-team/issues/10820)
to the feature details

These changes add a functionality which allows user to retry failed
migration rules.

### Other tasks and fixes

* Integrated `MigrationReadyPanel` and `MigrationProgressPanel` to show
migration's `ready` and `running` states
* Migration stats pooling issue caused by waiting while there are no
pending migrations left. If any other operation triggers `startPooling`
during the waiting it will be ignored and thus latest stats will never
come back.

> [!NOTE]  
> This feature needs `siemMigrationsEnabled` experimental flag enabled
to work.

### Testing note

1. Make sure you have a SIEM migration with failed rules
2. Open that migration via `Security > Rules > SIEM Rules Migrations >
{#MIGRATION_WITH_FAILED_RULES}`
3. You should see a `Reprocess rules (#)` button which triggers failed
rules reprocessing

## Screen recording


https://github.com/user-attachments/assets/d33dc4a0-1791-4869-aa8d-b0322b5f19c3

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ievgen Sorokopud 2025-01-08 13:21:17 +01:00 committed by GitHub
parent 83651607ee
commit e4586dac80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 527 additions and 123 deletions

View file

@ -383,6 +383,9 @@ import type {
InstallMigrationRulesResponse,
InstallTranslatedMigrationRulesRequestParamsInput,
InstallTranslatedMigrationRulesResponse,
RetryRuleMigrationRequestParamsInput,
RetryRuleMigrationRequestBodyInput,
RetryRuleMigrationResponse,
StartRuleMigrationRequestParamsInput,
StartRuleMigrationRequestBodyInput,
StartRuleMigrationResponse,
@ -2010,6 +2013,22 @@ detection engine rules.
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retries a SIEM rules migration using the migration id provided
*/
async retryRuleMigration(props: RetryRuleMigrationProps) {
this.log.info(`${new Date().toISOString()} Calling API RetryRuleMigration`);
return this.kbnClient
.request<RetryRuleMigrationResponse>({
path: replaceParams('/internal/siem_migrations/rules/{migration_id}/retry', props.params),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'PUT',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
async riskEngineGetPrivileges() {
this.log.info(`${new Date().toISOString()} Calling API RiskEngineGetPrivileges`);
return this.kbnClient
@ -2545,6 +2564,10 @@ export interface ReadRuleProps {
export interface ResolveTimelineProps {
query: ResolveTimelineRequestQueryInput;
}
export interface RetryRuleMigrationProps {
params: RetryRuleMigrationRequestParamsInput;
body: RetryRuleMigrationRequestBodyInput;
}
export interface RulePreviewProps {
query: RulePreviewRequestQueryInput;
body: RulePreviewRequestBodyInput;

View file

@ -214,6 +214,35 @@ export const InstallTranslatedMigrationRulesResponse = z.object({
installed: z.boolean(),
});
export type RetryRuleMigrationRequestParams = z.infer<typeof RetryRuleMigrationRequestParams>;
export const RetryRuleMigrationRequestParams = z.object({
migration_id: NonEmptyString,
});
export type RetryRuleMigrationRequestParamsInput = z.input<typeof RetryRuleMigrationRequestParams>;
export type RetryRuleMigrationRequestBody = z.infer<typeof RetryRuleMigrationRequestBody>;
export const RetryRuleMigrationRequestBody = z.object({
connector_id: ConnectorId,
langsmith_options: LangSmithOptions.optional(),
/**
* The indicator to retry only failed rules
*/
failed: z.boolean().optional(),
/**
* The indicator to retry only not fully translated rules
*/
not_fully_translated: z.boolean().optional(),
});
export type RetryRuleMigrationRequestBodyInput = z.input<typeof RetryRuleMigrationRequestBody>;
export type RetryRuleMigrationResponse = z.infer<typeof RetryRuleMigrationResponse>;
export const RetryRuleMigrationResponse = z.object({
/**
* Indicates the migration retry has been started. `false` means the migration does not need to be retried.
*/
started: z.boolean(),
});
export type StartRuleMigrationRequestParams = z.infer<typeof StartRuleMigrationRequestParams>;
export const StartRuleMigrationRequestParams = z.object({
migration_id: NonEmptyString,

View file

@ -286,6 +286,57 @@ paths:
204:
description: Indicates the migration id was not found.
/internal/siem_migrations/rules/{migration_id}/retry:
put:
summary: Retries a rule migration
operationId: RetryRuleMigration
x-codegen-enabled: true
x-internal: true
description: Retries a SIEM rules migration using the migration id provided
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to retry
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- connector_id
properties:
connector_id:
$ref: '../../common.schema.yaml#/components/schemas/ConnectorId'
langsmith_options:
$ref: '../../common.schema.yaml#/components/schemas/LangSmithOptions'
failed:
type: boolean
description: The indicator to retry only failed rules
not_fully_translated:
type: boolean
description: The indicator to retry only not fully translated rules
responses:
200:
description: Indicates the migration retry request has been processed successfully.
content:
application/json:
schema:
type: object
required:
- started
properties:
started:
type: boolean
description: Indicates the migration retry has been started. `false` means the migration does not need to be retried.
204:
description: Indicates the migration id was not found.
/internal/siem_migrations/rules/{migration_id}/stats:
get:
summary: Gets a rule migration task stats

View file

@ -23,6 +23,7 @@ import {
SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH,
SIEM_RULE_MIGRATION_RESOURCES_PATH,
SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH,
SIEM_RULE_MIGRATION_RETRY_PATH,
} from '../../../../common/siem_migrations/constants';
import type {
CreateRuleMigrationRequestBody,
@ -39,6 +40,9 @@ import type {
UpsertRuleMigrationResourcesResponse,
GetRuleMigrationPrebuiltRulesResponse,
UpdateRuleMigrationResponse,
RetryRuleMigrationRequestBody,
StartRuleMigrationResponse,
RetryRuleMigrationResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
export interface GetRuleMigrationStatsParams {
@ -146,17 +150,54 @@ export const startRuleMigration = async ({
connectorId,
langSmithOptions,
signal,
}: StartRuleMigrationParams): Promise<GetAllStatsRuleMigrationResponse> => {
}: StartRuleMigrationParams): Promise<StartRuleMigrationResponse> => {
const body: StartRuleMigrationRequestBody = { connector_id: connectorId };
if (langSmithOptions) {
body.langsmith_options = langSmithOptions;
}
return KibanaServices.get().http.put<GetAllStatsRuleMigrationResponse>(
return KibanaServices.get().http.put<StartRuleMigrationResponse>(
replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }),
{ body: JSON.stringify(body), version: '1', signal }
);
};
export interface RetryRuleMigrationParams {
/** `id` of the migration to reprocess rules for */
migrationId: string;
/** The connector id to use for the reprocessing */
connectorId: string;
/** Optional LangSmithOptions to use for the for the reprocessing */
langSmithOptions?: LangSmithOptions;
/** Optional indicator to retry only failed rules */
failed?: boolean;
/** Optional indicator to retry only not fully translated rules */
notFullyTranslated?: boolean;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}
/** Starts a reprocessing of migration rules in a specific migration. */
export const retryRuleMigration = async ({
migrationId,
connectorId,
langSmithOptions,
failed,
notFullyTranslated,
signal,
}: RetryRuleMigrationParams): Promise<RetryRuleMigrationResponse> => {
const body: RetryRuleMigrationRequestBody = {
connector_id: connectorId,
failed,
not_fully_translated: notFullyTranslated,
};
if (langSmithOptions) {
body.langsmith_options = langSmithOptions;
}
return KibanaServices.get().http.put<RetryRuleMigrationResponse>(
replaceParams(SIEM_RULE_MIGRATION_RETRY_PATH, { migration_id: migrationId }),
{ body: JSON.stringify(body), version: '1', signal }
);
};
export interface GetRuleMigrationParams {
/** `id` of the migration to get rules documents for */
migrationId: string;

View file

@ -6,21 +6,17 @@
*/
import React from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
} from '@elastic/eui';
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import * as i18n from './translations';
export interface BulkActionsProps {
isTableLoading: boolean;
numberOfFailedRules: number;
numberOfTranslatedRules: number;
numberOfSelectedRules: number;
installTranslatedRule?: () => void;
installSelectedRule?: () => void;
reprocessFailedRules?: () => void;
}
/**
@ -29,43 +25,60 @@ export interface BulkActionsProps {
export const BulkActions: React.FC<BulkActionsProps> = React.memo(
({
isTableLoading,
numberOfFailedRules,
numberOfTranslatedRules,
numberOfSelectedRules,
installTranslatedRule,
installSelectedRule,
reprocessFailedRules,
}) => {
const disableInstallTranslatedRulesButton = isTableLoading || !numberOfTranslatedRules;
const showInstallSelectedRulesButton = isTableLoading || numberOfSelectedRules > 0;
const showInstallSelectedRulesButton = numberOfSelectedRules > 0;
const showRetryFailedRulesButton = numberOfFailedRules > 0;
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{showInstallSelectedRulesButton ? (
{showInstallSelectedRulesButton && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="plusInCircle"
color={'primary'}
onClick={installSelectedRule}
onClick={() => installSelectedRule?.()}
disabled={isTableLoading}
isLoading={isTableLoading}
data-test-subj="installSelectedRulesButton"
aria-label={i18n.INSTALL_SELECTED_ARIA_LABEL}
>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
{isTableLoading && <EuiLoadingSpinner size="s" />}
</EuiButtonEmpty>
</EuiFlexItem>
) : null}
)}
{showRetryFailedRulesButton && (
<EuiFlexItem grow={false}>
<EuiButton
iconType="refresh"
color={'warning'}
onClick={() => reprocessFailedRules?.()}
disabled={isTableLoading}
isLoading={isTableLoading}
data-test-subj="reprocessFailedRulesButton"
aria-label={i18n.REPROCESS_FAILED_ARIA_LABEL}
>
{i18n.REPROCESS_FAILED_RULES(numberOfFailedRules)}
</EuiButton>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="plusInCircle"
data-test-subj="installTranslatedRulesButton"
onClick={installTranslatedRule}
onClick={() => installTranslatedRule?.()}
disabled={disableInstallTranslatedRulesButton}
isLoading={isTableLoading}
data-test-subj="installTranslatedRulesButton"
aria-label={i18n.INSTALL_TRANSLATED_ARIA_LABEL}
>
{numberOfTranslatedRules > 0
? i18n.INSTALL_TRANSLATED_RULES(numberOfTranslatedRules)
: i18n.INSTALL_TRANSLATED_RULES_EMPTY_STATE}
{isTableLoading && <EuiLoadingSpinner size="s" />}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -33,6 +33,7 @@ import { BulkActions } from './bulk_actions';
import { SearchField } from './search_field';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
import { useRetryRuleMigration } from '../../service/hooks/use_retry_rules';
const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_SORT_FIELD = 'translation_result';
@ -43,13 +44,18 @@ export interface MigrationRulesTableProps {
* Selected rule migration id
*/
migrationId: string;
/**
* Re-fetches latest rule migration data
*/
refetchData?: () => void;
}
/**
* Table Component for displaying SIEM rules migrations
*/
export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.memo(
({ migrationId }) => {
({ migrationId, refetchData }) => {
const { addError } = useAppToasts();
const [pageIndex, setPageIndex] = useState(0);
@ -132,6 +138,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
const { mutateAsync: installMigrationRules } = useInstallMigrationRules(migrationId);
const { mutateAsync: installTranslatedMigrationRules } =
useInstallTranslatedMigrationRules(migrationId);
const { retryRuleMigration, isLoading: isRetryLoading } = useRetryRuleMigration(refetchData);
const [isTableLoading, setTableLoading] = useState(false);
const installSingleRule = useCallback(
@ -180,7 +187,12 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
[addError, installTranslatedMigrationRules]
);
const isLoading = isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading;
const reprocessFailedRules = useCallback(async () => {
retryRuleMigration(migrationId, { failed: true });
}, [migrationId, retryRuleMigration]);
const isLoading =
isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading || isRetryLoading;
const ruleActionsFactory = useCallback(
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
@ -268,10 +280,12 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
<EuiFlexItem grow={false}>
<BulkActions
isTableLoading={isLoading}
numberOfTranslatedRules={translationStats?.rules.success.installable ?? 0}
numberOfFailedRules={translationStats.rules.failed}
numberOfTranslatedRules={translationStats.rules.success.installable}
numberOfSelectedRules={selectedRuleMigrations.length}
installTranslatedRule={installTranslatedRules}
installSelectedRule={installSelectedRule}
reprocessFailedRules={reprocessFailedRules}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -49,6 +49,13 @@ export const INSTALL_SELECTED_RULES = (numberOfSelectedRules: number) => {
});
};
export const REPROCESS_FAILED_RULES = (numberOfFailedRules: number) => {
return i18n.translate('xpack.securitySolution.siemMigrations.rules.table.reprocessFailedRules', {
defaultMessage: 'Reprocess rules ({numberOfFailedRules})',
values: { numberOfFailedRules },
});
};
export const INSTALL_TRANSLATED_RULES_EMPTY_STATE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.installTranslatedRulesEmptyState',
{
@ -81,6 +88,13 @@ export const INSTALL_TRANSLATED_ARIA_LABEL = i18n.translate(
}
);
export const REPROCESS_FAILED_ARIA_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.reprocessFailedRulesButtonAriaLabel',
{
defaultMessage: 'Reprocess failed rules',
}
);
export const ALREADY_TRANSLATED_RULE_TOOLTIP = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.alreadyTranslatedTooltip',
{

View file

@ -7,12 +7,19 @@
import React from 'react';
import { EuiLink } from '@elastic/eui';
import {
RuleTranslationResult,
SiemMigrationStatus,
} from '../../../../../common/siem_migrations/constants';
import { getRuleDetailsUrl } from '../../../../common/components/link_to';
import { useKibana } from '../../../../common/lib/kibana';
import { APP_UI_ID, SecurityPageName } from '../../../../../common';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import {
RuleMigrationStatusEnum,
type RuleMigration,
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
import { type TableColumn } from './constants';
interface ActionNameProps {
disableActions?: boolean;
@ -46,7 +53,7 @@ const ActionName = ({
);
}
if (migrationRule.status === 'failed') {
if (migrationRule.status === SiemMigrationStatus.FAILED) {
return (
<EuiLink disabled={disableActions} onClick={() => {}} data-test-subj="restartRule">
{i18n.ACTIONS_RESTART_LABEL}
@ -54,7 +61,7 @@ const ActionName = ({
);
}
if (migrationRule.translation_result === 'full') {
if (migrationRule.translation_result === RuleTranslationResult.FULL) {
return (
<EuiLink
disabled={disableActions}
@ -95,11 +102,11 @@ export const createActionsColumn = ({
return {
field: 'elastic_rule',
name: i18n.COLUMN_ACTIONS,
render: (value: RuleMigration['elastic_rule'], migrationRule: RuleMigration) => {
return (
render: (_, rule: RuleMigration) => {
return rule.status === RuleMigrationStatusEnum.failed ? null : (
<ActionName
disableActions={disableActions}
migrationRule={migrationRule}
migrationRule={rule}
openMigrationRuleDetails={openMigrationRuleDetails}
installMigrationRule={installMigrationRule}
/>

View file

@ -7,9 +7,10 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
const Author = ({ isPrebuiltRule }: { isPrebuiltRule: boolean }) => {
return (
@ -31,7 +32,11 @@ export const createAuthorColumn = (): TableColumn => {
field: 'elastic_rule.prebuilt_rule_id',
name: i18n.COLUMN_AUTHOR,
render: (_, rule: RuleMigration) => {
return <Author isPrebuiltRule={!!rule.elastic_rule?.prebuilt_rule_id} />;
return rule.status === SiemMigrationStatus.FAILED ? (
<>{COLUMN_EMPTY_VALUE}</>
) : (
<Author isPrebuiltRule={!!rule.elastic_rule?.prebuilt_rule_id} />
);
},
sortable: true,
width: '10%',

View file

@ -9,3 +9,5 @@ import type { EuiBasicTableColumn } from '@elastic/eui';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
export type TableColumn = EuiBasicTableColumn<RuleMigration>;
export const COLUMN_EMPTY_VALUE = '-';

View file

@ -7,7 +7,8 @@
import React from 'react';
import { EuiLink, EuiText } from '@elastic/eui';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
@ -17,7 +18,7 @@ interface NameProps {
}
const Name = ({ rule, openMigrationRuleDetails }: NameProps) => {
if (!rule.elastic_rule) {
if (rule.status === SiemMigrationStatus.FAILED) {
return (
<EuiText color="danger" size="s">
{rule.original_rule.title}
@ -31,7 +32,7 @@ const Name = ({ rule, openMigrationRuleDetails }: NameProps) => {
}}
data-test-subj="ruleName"
>
{rule.elastic_rule.title}
{rule.elastic_rule?.title ?? rule.original_rule.title}
</EuiLink>
);
};

View file

@ -7,17 +7,23 @@
import React from 'react';
import { EuiText } from '@elastic/eui';
import { DEFAULT_TRANSLATION_RISK_SCORE } from '../../../../../common/siem_migrations/constants';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import {
DEFAULT_TRANSLATION_RISK_SCORE,
SiemMigrationStatus,
} from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
import type { TableColumn } from './constants';
import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
export const createRiskScoreColumn = (): TableColumn => {
return {
field: 'risk_score',
name: i18n.COLUMN_RISK_SCORE,
render: () => (
render: (_, rule: RuleMigration) => (
<EuiText data-test-subj="riskScore" size="s">
{DEFAULT_TRANSLATION_RISK_SCORE}
{rule.status === SiemMigrationStatus.FAILED
? COLUMN_EMPTY_VALUE
: DEFAULT_TRANSLATION_RISK_SCORE}
</EuiText>
),
sortable: true,

View file

@ -7,16 +7,25 @@
import React from 'react';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { DEFAULT_TRANSLATION_SEVERITY } from '../../../../../common/siem_migrations/constants';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import {
DEFAULT_TRANSLATION_SEVERITY,
SiemMigrationStatus,
} from '../../../../../common/siem_migrations/constants';
import { SeverityBadge } from '../../../../common/components/severity_badge';
import type { TableColumn } from './constants';
import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
import * as i18n from './translations';
export const createSeverityColumn = (): TableColumn => {
return {
field: 'elastic_rule.severity',
name: i18n.COLUMN_SEVERITY,
render: (value?: Severity) => <SeverityBadge value={value ?? DEFAULT_TRANSLATION_SEVERITY} />,
render: (value: Severity = DEFAULT_TRANSLATION_SEVERITY, rule: RuleMigration) =>
rule.status === SiemMigrationStatus.FAILED ? (
<>{COLUMN_EMPTY_VALUE}</>
) : (
<SeverityBadge value={value} />
),
sortable: true,
truncateText: true,
width: '12%',

View file

@ -41,3 +41,10 @@ export const UPDATE_MIGRATION_RULES_FAILURE = i18n.translate(
defaultMessage: 'Failed to update migration rules',
}
);
export const RETRY_FAILED_RULES_FAILURE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.retryFailedRulesFailDescription',
{
defaultMessage: 'Failed to reprocess migration rules',
}
);

View file

@ -52,20 +52,23 @@ export const useGetMigrationRules = (params: {
*
* @returns A rule migrations cache invalidation callback
*/
export const useInvalidateGetMigrationRules = (migrationId: string) => {
export const useInvalidateGetMigrationRules = () => {
const queryClient = useQueryClient();
const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, {
migration_id: migrationId,
});
return useCallback(
(migrationId: string) => {
const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, {
migration_id: migrationId,
});
return useCallback(() => {
/**
* Invalidate all queries that start with SPECIFIC_MIGRATION_PATH. This
* includes the in-memory query cache and paged query cache.
*/
queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_PATH], {
refetchType: 'active',
});
}, [SPECIFIC_MIGRATION_PATH, queryClient]);
/**
* Invalidate all queries that start with SPECIFIC_MIGRATION_PATH. This
* includes the in-memory query cache and paged query cache.
*/
queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_PATH], {
refetchType: 'active',
});
},
[queryClient]
);
};

View file

@ -44,19 +44,22 @@ export const useGetMigrationTranslationStats = (migrationId: string) => {
*
* @returns A translation stats cache invalidation callback
*/
export const useInvalidateGetMigrationTranslationStats = (migrationId: string) => {
export const useInvalidateGetMigrationTranslationStats = () => {
const queryClient = useQueryClient();
const SPECIFIC_MIGRATION_TRANSLATION_PATH = replaceParams(
SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
{
migration_id: migrationId,
}
);
return useCallback(
(migrationId: string) => {
const SPECIFIC_MIGRATION_TRANSLATION_PATH = replaceParams(
SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
{
migration_id: migrationId,
}
);
return useCallback(() => {
queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_TRANSLATION_PATH], {
refetchType: 'active',
});
}, [SPECIFIC_MIGRATION_TRANSLATION_PATH, queryClient]);
queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_TRANSLATION_PATH], {
refetchType: 'active',
});
},
[queryClient]
);
};

View file

@ -19,9 +19,8 @@ export const INSTALL_MIGRATION_RULES_MUTATION_KEY = ['POST', SIEM_RULE_MIGRATION
export const useInstallMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(migrationId);
const invalidateGetMigrationTranslationStats =
useInvalidateGetMigrationTranslationStats(migrationId);
const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
return useMutation<InstallMigrationRulesResponse, Error, { ids: string[]; enabled: boolean }>(
({ ids, enabled = false }) => installMigrationRules({ migrationId, ids, enabled }),
@ -31,8 +30,8 @@ export const useInstallMigrationRules = (migrationId: string) => {
addError(error, { title: i18n.INSTALL_MIGRATION_RULES_FAILURE });
},
onSettled: () => {
invalidateGetRuleMigrations();
invalidateGetMigrationTranslationStats();
invalidateGetRuleMigrations(migrationId);
invalidateGetMigrationTranslationStats(migrationId);
},
}
);

View file

@ -22,9 +22,8 @@ export const INSTALL_ALL_MIGRATION_RULES_MUTATION_KEY = [
export const useInstallTranslatedMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(migrationId);
const invalidateGetMigrationTranslationStats =
useInvalidateGetMigrationTranslationStats(migrationId);
const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
return useMutation<InstallTranslatedMigrationRulesResponse, Error>(
() => installTranslatedMigrationRules({ migrationId }),
@ -34,8 +33,8 @@ export const useInstallTranslatedMigrationRules = (migrationId: string) => {
addError(error, { title: i18n.INSTALL_MIGRATION_RULES_FAILURE });
},
onSettled: () => {
invalidateGetRuleMigrations();
invalidateGetMigrationTranslationStats();
invalidateGetRuleMigrations(migrationId);
invalidateGetMigrationTranslationStats(migrationId);
},
}
);

View file

@ -20,9 +20,8 @@ export const UPDATE_MIGRATION_RULES_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATIONS_
export const useUpdateMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(migrationId);
const invalidateGetMigrationTranslationStats =
useInvalidateGetMigrationTranslationStats(migrationId);
const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
return useMutation<UpdateRuleMigrationResponse, Error, UpdateRuleMigrationData[]>(
(rulesToUpdate) => updateMigrationRules({ rulesToUpdate }),
@ -32,8 +31,8 @@ export const useUpdateMigrationRules = (migrationId: string) => {
addError(error, { title: i18n.UPDATE_MIGRATION_RULES_FAILURE });
},
onSettled: () => {
invalidateGetRuleMigrations();
invalidateGetMigrationTranslationStats();
invalidateGetRuleMigrations(migrationId);
invalidateGetMigrationTranslationStats(migrationId);
},
}
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui';
import type { RouteComponentProps } from 'react-router-dom';
@ -22,6 +22,11 @@ import { MissingPrivilegesCallOut } from '../../../detections/components/callout
import { HeaderButtons } from '../components/header_buttons';
import { UnknownMigration } from '../components/unknown_migration';
import { useLatestStats } from '../service/hooks/use_latest_stats';
import { RuleMigrationDataInputWrapper } from '../components/data_input_flyout/data_input_wrapper';
import { MigrationReadyPanel } from '../components/migration_status_panels/migration_ready_panel';
import { MigrationProgressPanel } from '../components/migration_status_panels/migration_progress_panel';
import { useInvalidateGetMigrationRules } from '../logic/use_get_migration_rules';
import { useInvalidateGetMigrationTranslationStats } from '../logic/use_get_migration_translation_stats';
type MigrationRulesPageProps = RouteComponentProps<{ migrationId?: string }>;
@ -32,25 +37,15 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
},
}) => {
const { navigateTo } = useNavigation();
const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } = useLatestStats();
const finishedRuleMigrationsStats = useMemo(() => {
if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) {
return [];
}
return ruleMigrationsStatsAll.filter(
(migration) => migration.status === SiemMigrationTaskStatus.FINISHED
);
}, [isLoadingMigrationsStats, ruleMigrationsStatsAll]);
const { data: ruleMigrationsStats, isLoading, refreshStats } = useLatestStats();
useEffect(() => {
if (isLoadingMigrationsStats) {
if (isLoading) {
return;
}
// Navigate to landing page if there are no migrations
if (!finishedRuleMigrationsStats.length) {
if (!ruleMigrationsStats.length) {
navigateTo({ deepLinkId: SecurityPageName.landing, path: 'siem_migrations' });
return;
}
@ -59,21 +54,52 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
if (!migrationId) {
navigateTo({
deepLinkId: SecurityPageName.siemMigrationsRules,
path: finishedRuleMigrationsStats[0].id,
path: ruleMigrationsStats[0].id,
});
}
}, [isLoadingMigrationsStats, migrationId, finishedRuleMigrationsStats, navigateTo]);
}, [isLoading, migrationId, navigateTo, ruleMigrationsStats]);
const onMigrationIdChange = (selectedId?: string) => {
navigateTo({ deepLinkId: SecurityPageName.siemMigrationsRules, path: selectedId });
};
const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
const refetchData = useCallback(() => {
if (!migrationId) {
return;
}
refreshStats();
invalidateGetRuleMigrations(migrationId);
invalidateGetMigrationTranslationStats(migrationId);
}, [
invalidateGetMigrationTranslationStats,
invalidateGetRuleMigrations,
migrationId,
refreshStats,
]);
const content = useMemo(() => {
if (!migrationId || !finishedRuleMigrationsStats.some((stats) => stats.id === migrationId)) {
const migrationStats = ruleMigrationsStats.find((stats) => stats.id === migrationId);
if (!migrationId || !migrationStats) {
return <UnknownMigration />;
}
return <MigrationRulesTable migrationId={migrationId} />;
}, [migrationId, finishedRuleMigrationsStats]);
if (migrationStats.status === SiemMigrationTaskStatus.FINISHED) {
return <MigrationRulesTable migrationId={migrationId} refetchData={refetchData} />;
}
return (
<RuleMigrationDataInputWrapper onFlyoutClosed={refetchData}>
<>
{migrationStats.status === SiemMigrationTaskStatus.READY && (
<MigrationReadyPanel migrationStats={migrationStats} />
)}
{migrationStats.status === SiemMigrationTaskStatus.RUNNING && (
<MigrationProgressPanel migrationStats={migrationStats} />
)}
</>
</RuleMigrationDataInputWrapper>
);
}, [migrationId, refetchData, ruleMigrationsStats]);
return (
<>
@ -83,13 +109,13 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
<SecuritySolutionPageWrapper>
<HeaderPage title={i18n.PAGE_TITLE}>
<HeaderButtons
ruleMigrationsStats={finishedRuleMigrationsStats}
ruleMigrationsStats={ruleMigrationsStats}
selectedMigrationId={migrationId}
onMigrationIdChange={onMigrationIdChange}
/>
</HeaderPage>
<EuiSkeletonLoading
isLoading={isLoadingMigrationsStats}
isLoading={isLoading}
loadingContent={
<>
<EuiSkeletonTitle />

View file

@ -0,0 +1,51 @@
/*
* 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, useReducer } from 'react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
import { reducer, initialState } from './common/api_request_reducer';
import type { RetryRuleMigrationFilter } from '../../types';
export const RETRY_RULE_MIGRATION_SUCCESS = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.service.retryMigrationRulesSuccess',
{ defaultMessage: 'Retry rule migration started successfully.' }
);
export const RETRY_RULE_MIGRATION_ERROR = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.service.retryMigrationRulesError',
{ defaultMessage: 'Error retrying a rule migration.' }
);
export type RetryRuleMigration = (migrationId: string, filter?: RetryRuleMigrationFilter) => void;
export type OnSuccess = () => void;
export const useRetryRuleMigration = (onSuccess?: OnSuccess) => {
const { siemMigrations, notifications } = useKibana().services;
const [state, dispatch] = useReducer(reducer, initialState);
const retryRuleMigration = useCallback<RetryRuleMigration>(
(migrationId, filter) => {
(async () => {
try {
dispatch({ type: 'start' });
await siemMigrations.rules.retryRuleMigration(migrationId, filter);
notifications.toasts.addSuccess(RETRY_RULE_MIGRATION_SUCCESS);
dispatch({ type: 'success' });
onSuccess?.();
} catch (err) {
const apiError = err.body ?? err;
notifications.toasts.addError(apiError, { title: RETRY_RULE_MIGRATION_ERROR });
dispatch({ type: 'error', error: apiError });
}
})();
},
[siemMigrations.rules, notifications.toasts, onSuccess]
);
return { isLoading: state.loading, error: state.error, retryRuleMigration };
};

View file

@ -19,8 +19,9 @@ import type {
} from '../../../../common/siem_migrations/model/rule_migration.gen';
import type {
CreateRuleMigrationRequestBody,
GetAllStatsRuleMigrationResponse,
GetRuleMigrationStatsResponse,
RetryRuleMigrationResponse,
StartRuleMigrationResponse,
UpsertRuleMigrationResourcesRequestBody,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants';
@ -35,8 +36,9 @@ import {
type GetRuleMigrationsStatsAllParams,
getMissingResources,
upsertMigrationResources,
retryRuleMigration,
} from '../api';
import type { RuleMigrationStats } from '../types';
import type { RetryRuleMigrationFilter, RuleMigrationStats } from '../types';
import { getSuccessToast } from './success_notification';
import { RuleMigrationsStorage } from './storage';
import * as i18n from './translations';
@ -119,7 +121,7 @@ export class SiemRulesMigrationsService {
}
}
public async startRuleMigration(migrationId: string): Promise<GetAllStatsRuleMigrationResponse> {
public async startRuleMigration(migrationId: string): Promise<StartRuleMigrationResponse> {
const connectorId = this.connectorIdStorage.get();
if (!connectorId) {
throw new Error(i18n.MISSING_CONNECTOR_ERROR);
@ -139,6 +141,34 @@ export class SiemRulesMigrationsService {
return result;
}
public async retryRuleMigration(
migrationId: string,
filter?: RetryRuleMigrationFilter
): Promise<RetryRuleMigrationResponse> {
const connectorId = this.connectorIdStorage.get();
if (!connectorId) {
throw new Error(i18n.MISSING_CONNECTOR_ERROR);
}
const langSmithSettings = this.traceOptionsStorage.get();
let langSmithOptions: LangSmithOptions | undefined;
if (langSmithSettings) {
langSmithOptions = {
project_name: langSmithSettings.langSmithProject,
api_key: langSmithSettings.langSmithApiKey,
};
}
const result = await retryRuleMigration({
migrationId,
connectorId,
langSmithOptions,
...filter,
});
this.startPolling();
return result;
}
public async getRuleMigrationStats(migrationId: string): Promise<GetRuleMigrationStatsResponse> {
return getRuleMigrationStats({ migrationId });
}
@ -213,7 +243,12 @@ export class SiemRulesMigrationsService {
}
}
await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_SECONDS * 1000));
// Do not wait if there are no more pending migrations
if (pendingMigrationIds.length > 0) {
await new Promise((resolve) =>
setTimeout(resolve, REQUEST_POLLING_INTERVAL_SECONDS * 1000)
);
}
} while (pendingMigrationIds.length > 0);
}
}

View file

@ -13,3 +13,8 @@ export interface RuleMigrationStats extends RuleMigrationTaskStats {
/** The sequential number of the migration */
number: number;
}
export interface RetryRuleMigrationFilter {
failed?: boolean;
notFullyTranslated?: boolean;
}

View file

@ -10,13 +10,14 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { APMTracer } from '@kbn/langchain/server/tracers/apm';
import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith';
import {
StartRuleMigrationRequestBody,
StartRuleMigrationRequestParams,
type StartRuleMigrationResponse,
RetryRuleMigrationRequestBody,
RetryRuleMigrationRequestParams,
type RetryRuleMigrationResponse,
} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_RETRY_PATH } from '../../../../../common/siem_migrations/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { withLicense } from './util/with_license';
import type { RuleMigrationFilters } from '../data/rule_migrations_data_rules_client';
export const registerSiemRuleMigrationsRetryRoute = (
router: SecuritySolutionPluginRouter,
@ -33,15 +34,20 @@ export const registerSiemRuleMigrationsRetryRoute = (
version: '1',
validate: {
request: {
params: buildRouteValidationWithZod(StartRuleMigrationRequestParams),
body: buildRouteValidationWithZod(StartRuleMigrationRequestBody),
params: buildRouteValidationWithZod(RetryRuleMigrationRequestParams),
body: buildRouteValidationWithZod(RetryRuleMigrationRequestBody),
},
},
},
withLicense(
async (context, req, res): Promise<IKibanaResponse<StartRuleMigrationResponse>> => {
async (context, req, res): Promise<IKibanaResponse<RetryRuleMigrationResponse>> => {
const migrationId = req.params.migration_id;
const { langsmith_options: langsmithOptions, connector_id: connectorId } = req.body;
const {
langsmith_options: langsmithOptions,
connector_id: connectorId,
failed,
not_fully_translated: notFullyTranslated,
} = req.body;
try {
const ctx = await context.resolve(['core', 'actions', 'alerting', 'securitySolution']);
@ -59,7 +65,8 @@ export const registerSiemRuleMigrationsRetryRoute = (
],
};
const { updated } = await ruleMigrationsClient.task.updateToRetry(migrationId);
const filters: RuleMigrationFilters = { failed, notFullyTranslated };
const { updated } = await ruleMigrationsClient.task.updateToRetry(migrationId, filters);
if (!updated) {
return res.ok({ body: { started: false } });
}

View file

@ -43,6 +43,8 @@ export interface RuleMigrationFilters {
ids?: string[];
installable?: boolean;
prebuilt?: boolean;
failed?: boolean;
notFullyTranslated?: boolean;
searchTerm?: string;
}
export interface RuleMigrationGetOptions {
@ -239,7 +241,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
async releaseProcessing(migrationId: string): Promise<void> {
return this.updateStatus(
migrationId,
SiemMigrationStatus.PROCESSING,
{ status: SiemMigrationStatus.PROCESSING },
SiemMigrationStatus.PENDING
);
}
@ -247,12 +249,12 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
/** Updates all the rule migration with the provided id and with status `statusToQuery` to `statusToUpdate` */
async updateStatus(
migrationId: string,
statusToQuery: SiemMigrationStatus | SiemMigrationStatus[] | undefined,
filter: RuleMigrationFilters,
statusToUpdate: SiemMigrationStatus,
{ refresh = false }: { refresh?: boolean } = {}
): Promise<void> {
const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId, { status: statusToQuery });
const query = this.getFilterQuery(migrationId, filter);
const script = { source: `ctx._source['status'] = '${statusToUpdate}'` };
await this.esClient.updateByQuery({ index, query, script, refresh }).catch((error) => {
this.logger.error(`Error updating rule migrations status: ${error.message}`);
@ -397,7 +399,15 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
private getFilterQuery(
migrationId: string,
{ status, ids, installable, prebuilt, searchTerm }: RuleMigrationFilters = {}
{
status,
ids,
installable,
prebuilt,
searchTerm,
failed,
notFullyTranslated,
}: RuleMigrationFilters = {}
): QueryDslQueryContainer {
const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }];
if (status) {
@ -419,6 +429,12 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
if (searchTerm?.length) {
filter.push(searchConditions.matchTitle(searchTerm));
}
if (failed) {
filter.push(searchConditions.isFailed());
}
if (notFullyTranslated) {
filter.push(searchConditions.isNotFullyTranslated());
}
return { bool: { filter } };
}
}

View file

@ -6,12 +6,18 @@
*/
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import {
RuleTranslationResult,
SiemMigrationStatus,
} from '../../../../../common/siem_migrations/constants';
export const conditions = {
isFullyTranslated(): QueryDslQueryContainer {
return { term: { translation_result: RuleTranslationResult.FULL } };
},
isNotFullyTranslated(): QueryDslQueryContainer {
return { bool: { must_not: conditions.isFullyTranslated() } };
},
isNotInstalled(): QueryDslQueryContainer {
return {
nested: {
@ -39,4 +45,7 @@ export const conditions = {
isInstallable(): QueryDslQueryContainer[] {
return [this.isFullyTranslated(), this.isNotInstalled()];
},
isFailed(): QueryDslQueryContainer {
return { term: { status: SiemMigrationStatus.FAILED } };
},
};

View file

@ -14,7 +14,10 @@ import {
} from '../../../../../common/siem_migrations/constants';
import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationsDataClient } from '../data/rule_migrations_data_client';
import type { RuleMigrationDataStats } from '../data/rule_migrations_data_rules_client';
import type {
RuleMigrationDataStats,
RuleMigrationFilters,
} from '../data/rule_migrations_data_rules_client';
import { getRuleMigrationAgent } from './agent';
import type { MigrateRuleState } from './agent/types';
import { RuleMigrationsRetriever } from './retrievers';
@ -49,7 +52,7 @@ export class RuleMigrationsTaskClient {
// Just in case some previous execution was interrupted without cleaning up
await this.data.rules.updateStatus(
migrationId,
SiemMigrationStatus.PROCESSING,
{ status: SiemMigrationStatus.PROCESSING },
SiemMigrationStatus.PENDING,
{ refresh: true }
);
@ -203,12 +206,15 @@ export class RuleMigrationsTaskClient {
}
/** Updates all the rules in a migration to be re-executed */
public async updateToRetry(migrationId: string): Promise<{ updated: boolean }> {
public async updateToRetry(
migrationId: string,
filter: RuleMigrationFilters = {}
): Promise<{ updated: boolean }> {
if (this.migrationsRunning.has(migrationId)) {
return { updated: false };
}
// Update all the rules in the migration to pending
await this.data.rules.updateStatus(migrationId, undefined, SiemMigrationStatus.PENDING, {
await this.data.rules.updateStatus(migrationId, filter, SiemMigrationStatus.PENDING, {
refresh: true,
});
return { updated: true };

View file

@ -138,6 +138,10 @@ import { PreviewRiskScoreRequestBodyInput } from '@kbn/security-solution-plugin/
import { ReadAlertsMigrationStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/read_signals_migration_status/read_signals_migration_status.gen';
import { ReadRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/read_rule/read_rule_route.gen';
import { ResolveTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/resolve_timeline/resolve_timeline_route.gen';
import {
RetryRuleMigrationRequestParamsInput,
RetryRuleMigrationRequestBodyInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
RulePreviewRequestQueryInput,
RulePreviewRequestBodyInput,
@ -1402,6 +1406,22 @@ detection engine rules.
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query);
},
/**
* Retries a SIEM rules migration using the migration id provided
*/
retryRuleMigration(props: RetryRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest
.put(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}/retry', props.params),
kibanaSpace
)
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
riskEngineGetPrivileges(kibanaSpace: string = 'default') {
return supertest
.get(routeWithNamespace('/internal/risk_engine/privileges', kibanaSpace))
@ -1875,6 +1895,10 @@ export interface ReadRuleProps {
export interface ResolveTimelineProps {
query: ResolveTimelineRequestQueryInput;
}
export interface RetryRuleMigrationProps {
params: RetryRuleMigrationRequestParamsInput;
body: RetryRuleMigrationRequestBodyInput;
}
export interface RulePreviewProps {
query: RulePreviewRequestQueryInput;
body: RulePreviewRequestBodyInput;