Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import * as i18n from './translations';

export interface BulkActionsProps {
isTableLoading: boolean;
numberOfFailedRules: number;
numberOfTranslatedRules: number;
numberOfSelectedRules: number;
installTranslatedRule?: () => void;
installSelectedRule?: () => void;
reprocessFailedRules?: () => void;
}

/**
Expand All @@ -29,13 +31,16 @@ 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 showRetryFailedRulesButton = isTableLoading || numberOfFailedRules > 0;
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{showInstallSelectedRulesButton ? (
Expand All @@ -53,9 +58,23 @@ export const BulkActions: React.FC<BulkActionsProps> = React.memo(
</EuiButtonEmpty>
</EuiFlexItem>
) : null}
{showRetryFailedRulesButton ? (
e40pud marked this conversation as resolved.
Show resolved Hide resolved
<EuiFlexItem grow={false}>
<EuiButton
iconType="refresh"
color={'warning'}
data-test-subj="reprocessFailedRulesButton"
onClick={reprocessFailedRules}
disabled={isTableLoading}
aria-label={i18n.REPROCESS_FAILED_ARIA_LABEL}
>
{i18n.REPROCESS_FAILED_RULES(numberOfFailedRules)}
{isTableLoading && <EuiLoadingSpinner size="s" />}
e40pud marked this conversation as resolved.
Show resolved Hide resolved
</EuiButton>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="plusInCircle"
data-test-subj="installTranslatedRulesButton"
onClick={installTranslatedRule}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down Expand Up @@ -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',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,15 +53,15 @@ const ActionName = ({
);
}

if (migrationRule.status === 'failed') {
if (migrationRule.status === SiemMigrationStatus.FAILED) {
return (
<EuiLink disabled={disableActions} onClick={() => {}} data-test-subj="restartRule">
{i18n.ACTIONS_RESTART_LABEL}
</EuiLink>
);
}

if (migrationRule.translation_result === 'full') {
if (migrationRule.translation_result === RuleTranslationResult.FULL) {
return (
<EuiLink
disabled={disableActions}
Expand Down Expand Up @@ -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}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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%',
Expand Down
Loading