diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index fa315e3c421aa..9b0b853083b59 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -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({ + 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; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 47c06e1e02c7a..0c712e27cdbdb 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -214,6 +214,35 @@ export const InstallTranslatedMigrationRulesResponse = z.object({ installed: z.boolean(), }); +export type RetryRuleMigrationRequestParams = z.infer; +export const RetryRuleMigrationRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type RetryRuleMigrationRequestParamsInput = z.input; + +export type RetryRuleMigrationRequestBody = z.infer; +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; + +export type RetryRuleMigrationResponse = z.infer; +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; export const StartRuleMigrationRequestParams = z.object({ migration_id: NonEmptyString, diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index 69e43b57dabd3..d664c79993e5f 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -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 diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts index 02fb423b05279..2e027837623ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -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 => { +}: StartRuleMigrationParams): Promise => { const body: StartRuleMigrationRequestBody = { connector_id: connectorId }; if (langSmithOptions) { body.langsmith_options = langSmithOptions; } - return KibanaServices.get().http.put( + return KibanaServices.get().http.put( 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 => { + const body: RetryRuleMigrationRequestBody = { + connector_id: connectorId, + failed, + not_fully_translated: notFullyTranslated, + }; + if (langSmithOptions) { + body.langsmith_options = langSmithOptions; + } + return KibanaServices.get().http.put( + 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; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx index 8f32308ed52c4..60cd2418c8bf8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx @@ -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 = 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 ( - {showInstallSelectedRulesButton ? ( + {showInstallSelectedRulesButton && ( installSelectedRule?.()} disabled={isTableLoading} + isLoading={isTableLoading} data-test-subj="installSelectedRulesButton" aria-label={i18n.INSTALL_SELECTED_ARIA_LABEL} > {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} - {isTableLoading && } - ) : null} + )} + {showRetryFailedRulesButton && ( + + reprocessFailedRules?.()} + disabled={isTableLoading} + isLoading={isTableLoading} + data-test-subj="reprocessFailedRulesButton" + aria-label={i18n.REPROCESS_FAILED_ARIA_LABEL} + > + {i18n.REPROCESS_FAILED_RULES(numberOfFailedRules)} + + + )} 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 && } diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx index b883934a0bdcb..65a7628185bbe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx @@ -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 = React.memo( - ({ migrationId }) => { + ({ migrationId, refetchData }) => { const { addError } = useAppToasts(); const [pageIndex, setPageIndex] = useState(0); @@ -132,6 +138,7 @@ export const MigrationRulesTable: React.FC = 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 = 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 = React.mem diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts index 79b5a1fe00900..b553b6dfd7358 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts @@ -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', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx index 45de70582d4b1..a8175cbffc4db 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx @@ -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 ( {}} 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 ( { - return ( + render: (_, rule: RuleMigration) => { + return rule.status === RuleMigrationStatusEnum.failed ? null : ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx index 23980f5612f89..25aad27f2751c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx @@ -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 ; + return rule.status === SiemMigrationStatus.FAILED ? ( + <>{COLUMN_EMPTY_VALUE} + ) : ( + + ); }, sortable: true, width: '10%', diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx index 724e4dcb101a1..1576b13f76dc6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx @@ -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; + +export const COLUMN_EMPTY_VALUE = '-'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx index ce0e1d3c99d8d..56f4123403c97 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx @@ -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 ( {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} ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx index 0fb78ae8bf709..d0584cc14e2af 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx @@ -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) => ( - {DEFAULT_TRANSLATION_RISK_SCORE} + {rule.status === SiemMigrationStatus.FAILED + ? COLUMN_EMPTY_VALUE + : DEFAULT_TRANSLATION_RISK_SCORE} ), sortable: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx index 9a6c0b98ff317..2a97288eef267 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx @@ -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) => , + render: (value: Severity = DEFAULT_TRANSLATION_SEVERITY, rule: RuleMigration) => + rule.status === SiemMigrationStatus.FAILED ? ( + <>{COLUMN_EMPTY_VALUE} + ) : ( + + ), sortable: true, truncateText: true, width: '12%', diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts index ef3521fd37301..e83293ec61097 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts @@ -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', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts index b06f041e2c58e..ffa382e5a3a40 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts @@ -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] + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_translation_stats.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_translation_stats.ts index b19a1133e3061..0111c3382cfe2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_translation_stats.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_translation_stats.ts @@ -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] + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts index 2b28b3b944990..b69be3b86d11c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts @@ -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( ({ 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); }, } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_translated_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_translated_migration_rules.ts index b0d9e11136396..bcce981a4cfb5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_translated_migration_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_translated_migration_rules.ts @@ -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( () => 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); }, } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts index 1e0fa22c466f0..2c39da63f8374 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts @@ -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( (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); }, } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx index 3877a6f46cbe7..f0f14bce70bbf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -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 = 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 = 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 ; } - return ; - }, [migrationId, finishedRuleMigrationsStats]); + if (migrationStats.status === SiemMigrationTaskStatus.FINISHED) { + return ; + } + return ( + + <> + {migrationStats.status === SiemMigrationTaskStatus.READY && ( + + )} + {migrationStats.status === SiemMigrationTaskStatus.RUNNING && ( + + )} + + + ); + }, [migrationId, refetchData, ruleMigrationsStats]); return ( <> @@ -83,13 +109,13 @@ export const MigrationRulesPage: React.FC = React.memo( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts new file mode 100644 index 0000000000000..6755d2da738a6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts @@ -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( + (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 }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 83ead556b09cc..21642c87fde11 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -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 { + public async startRuleMigration(migrationId: string): Promise { 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 { + 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 { 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); } } diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts index bcc11327d1051..b852a62e8c29f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts @@ -13,3 +13,8 @@ export interface RuleMigrationStats extends RuleMigrationTaskStats { /** The sequential number of the migration */ number: number; } + +export interface RetryRuleMigrationFilter { + failed?: boolean; + notFullyTranslated?: boolean; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts index 0fb96d9aaf72c..0062e21a8124f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts @@ -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> => { + async (context, req, res): Promise> => { 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 } }); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 47bcd56e6433e..e03f334209377 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -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 { 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 { 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 } }; } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts index 3bd8da066a45f..34db599df8edc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts @@ -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 } }; + }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index 1edd1b449070c..663a8f5218f33 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -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 }; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 9ffdc1c43a2a1..e2bc6fd051a48 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -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;