Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40872,7 +40872,7 @@
"xpack.securitySolution.siemMigrations.rules.panel.progress.description": "Traitement de la migration de {totalRules} règles.",
"xpack.securitySolution.siemMigrations.rules.panel.progress.preparing": "Préparation de l'environnement pour la traduction alimentée par l'IA.",
"xpack.securitySolution.siemMigrations.rules.panel.progress.translating": "Traduction des règles",
"xpack.securitySolution.siemMigrations.rules.panel.ready.description": "La migration de {totalRules} règles est créée, mais la traduction n'a pas encore commencé. {missingResourcesText}",
"xpack.securitySolution.siemMigrations.rules.panel.ready.description": "La migration de {totalRules} règles est créée, mais la traduction n'a pas encore commencé.",
"xpack.securitySolution.siemMigrations.rules.panel.ready.missingResources": "Chargez les macros et les consultations et débutez le processus de traduction",
"xpack.securitySolution.siemMigrations.rules.panel.result.badge": "Traduction terminée",
"xpack.securitySolution.siemMigrations.rules.panel.result.description": "L'exportation a été téléchargée à {createdAt} et la traduction s'est terminée à {finishedAt}.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40836,7 +40836,7 @@
"xpack.securitySolution.siemMigrations.rules.panel.progress.description": "{totalRules}ルールの移行を処理しています。",
"xpack.securitySolution.siemMigrations.rules.panel.progress.preparing": "AIを活用した変換の環境を準備しています。",
"xpack.securitySolution.siemMigrations.rules.panel.progress.translating": "ルールを変換中",
"xpack.securitySolution.siemMigrations.rules.panel.ready.description": "{totalRules}のルールの移行は作成されていますが、変換はまだ開始されていません。{missingResourcesText}",
"xpack.securitySolution.siemMigrations.rules.panel.ready.description": "{totalRules}のルールの移行は作成されていますが、変換はまだ開始されていません。",
"xpack.securitySolution.siemMigrations.rules.panel.ready.missingResources": "マクロとルックアップをアップロードし、変換プロセスを開始",
"xpack.securitySolution.siemMigrations.rules.panel.result.badge": "変換完了",
"xpack.securitySolution.siemMigrations.rules.panel.result.description": "{createdAt}にアップロードされたエクスポートと変換が{finishedAt}に完了しました。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40914,7 +40914,7 @@
"xpack.securitySolution.siemMigrations.rules.panel.progress.description": "正在处理 {totalRules} 个规则的迁移。",
"xpack.securitySolution.siemMigrations.rules.panel.progress.preparing": "正在准备环境以进行 AI 驱动式转换。",
"xpack.securitySolution.siemMigrations.rules.panel.progress.translating": "正在转换规则",
"xpack.securitySolution.siemMigrations.rules.panel.ready.description": "已创建 {totalRules} 个规则的迁移,但转换尚未开始。{missingResourcesText}",
"xpack.securitySolution.siemMigrations.rules.panel.ready.description": "已创建 {totalRules} 个规则的迁移,但转换尚未开始。",
"xpack.securitySolution.siemMigrations.rules.panel.ready.missingResources": "上传宏和查找,然后开始转换过程",
"xpack.securitySolution.siemMigrations.rules.panel.result.badge": "转换完成",
"xpack.securitySolution.siemMigrations.rules.panel.result.description": "导出已于 {createdAt} 上传,且转换已于 {finishedAt} 完成。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@ export const SIEM_RULE_MIGRATION_EVALUATE_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/e
export const LOOKUPS_INDEX_PREFIX = 'lookup_';

export enum SiemMigrationTaskStatus {
/** The Migration is not yet started */
READY = 'ready',
/** The Migration is in progress */
RUNNING = 'running',
/** The Migration process has been stopped for some reason unrelated to the user, usually a server restart. */
STOPPED = 'stopped',
/** The Migration is completed without any issues */
FINISHED = 'finished',
/** The Migration is explicitly aborted by user */
ABORTED = 'aborted',
}

export enum SiemMigrationStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,33 @@ export const PrebuiltRuleVersion = z.object({
current: RuleResponse.optional(),
});

/**
* The last execution of the rule migration task.
*/
export type RuleMigrationLastExecution = z.infer<typeof RuleMigrationLastExecution>;
export const RuleMigrationLastExecution = z.object({
/**
* The moment the last execution started.
*/
started_at: z.string().optional(),
/**
* The moment the last execution ended.
*/
ended_at: z.string().nullable().optional(),
/**
* The connector ID used for the last execution.
*/
connector_id: z.string().optional(),
/**
* The error message if the last execution failed.
*/
error: z.string().nullable().optional(),
/**
* Indicates if the last execution was aborted by the user.
*/
is_aborted: z.boolean().optional(),
});

/**
* The rule migration object ( without Id ) with its settings.
*/
Expand All @@ -154,6 +181,10 @@ export const RuleMigrationData = z.object({
* The moment migration was created
*/
created_at: NonEmptyString,
/**
* The last execution of the rule migration task.
*/
last_execution: RuleMigrationLastExecution.optional(),
});

/**
Expand Down Expand Up @@ -274,7 +305,13 @@ export const RuleMigrationRule = z
* The status of the migration task.
*/
export type RuleMigrationTaskStatus = z.infer<typeof RuleMigrationTaskStatus>;
export const RuleMigrationTaskStatus = z.enum(['ready', 'running', 'stopped', 'finished']);
export const RuleMigrationTaskStatus = z.enum([
'ready',
'running',
'stopped',
'finished',
'aborted',
]);
export type RuleMigrationTaskStatusEnum = typeof RuleMigrationTaskStatus.enum;
export const RuleMigrationTaskStatusEnum = RuleMigrationTaskStatus.enum;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ components:
created_at:
description: The moment migration was created
$ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
last_execution:
description: The last execution of the rule migration task.
$ref: '#/components/schemas/RuleMigrationLastExecution'


RuleMigrationRule:
Expand Down Expand Up @@ -256,6 +259,7 @@ components:
- running
- stopped
- finished
- aborted

RuleMigrationTranslationStats:
type: object
Expand Down Expand Up @@ -464,3 +468,25 @@ components:
updated_by:
type: string
description: The user who last updated the resource

RuleMigrationLastExecution:
type: object
description: The last execution of the rule migration task.
properties:
started_at:
type: string
description: The moment the last execution started.
ended_at:
type: string
nullable: true
description: The moment the last execution ended.
connector_id:
type: string
description: The connector ID used for the last execution.
error:
type: string
nullable: true
description: The error message if the last execution failed.
is_aborted:
type: boolean
description: Indicates if the last execution was aborted by the user.
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@ export const RuleMigrationsPanels = React.memo<RuleMigrationsPanelsProps>(
grow={false}
key={migrationStats.id}
>
{(migrationStats.status === SiemMigrationTaskStatus.READY ||
migrationStats.status === SiemMigrationTaskStatus.STOPPED) && (
{[
SiemMigrationTaskStatus.READY,
SiemMigrationTaskStatus.STOPPED,
SiemMigrationTaskStatus.ABORTED,
].includes(migrationStats.status) && (
<MigrationReadyPanel migrationStats={migrationStats} />
)}
{migrationStats.status === SiemMigrationTaskStatus.RUNNING && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ interface RuleMigrationsLastErrorProps {
}

export const RuleMigrationsLastError = React.memo<RuleMigrationsLastErrorProps>(({ message }) => (
<EuiCallOut title={i18n.RULE_MIGRATION_ERROR_TITLE} color="danger" iconType="alert" size="s">
<EuiCallOut
data-test-subj="ruleMigrationLastError"
title={i18n.RULE_MIGRATION_ERROR_TITLE}
color="danger"
iconType="alert"
size="s"
>
<EuiText size="s">{message}</EuiText>
</EuiCallOut>
));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { MigrationReadyPanel } from './migration_ready_panel';
import { useGetMissingResources } from '../../service/hooks/use_get_missing_resources';
import { useStartMigration } from '../../service/hooks/use_start_migration';
import { SiemMigrationTaskStatus } from '../../../../../common/siem_migrations/constants';
import type { RuleMigrationResourceBase } from '../../../../../common/siem_migrations/model/rule_migration.gen';

jest.mock('../data_input_flyout/context', () => ({
useRuleMigrationDataInputContext: () => ({
openFlyout: jest.fn(),
}),
}));

jest.mock('../../../../common/lib/kibana/kibana_react', () => ({
useKibana: jest.fn(() => ({
services: {
siemMigrations: {
rules: {
telemetry: jest.fn(),
},
},
},
})),
}));

jest.mock('../../service/hooks/use_start_migration');
const useStartMigrationMock = useStartMigration as jest.Mock;
const mockStartMigration = jest.fn();

const mockMigrationStateWithError = {
status: SiemMigrationTaskStatus.READY,
last_error:
'Failed to populate ELSER indices. Make sure the ELSER model is deployed and running at Machine Learning > Trained Models. Error: Exception when running inference id [.elser-2-elasticsearch] on field [elser_embedding]',
id: 'c44d2c7d-0de1-4231-8b82-0dcfd67a9fe3',
rules: { total: 6, pending: 6, processing: 0, completed: 0, failed: 0 },
created_at: '2025-05-27T12:12:17.563Z',
last_updated_at: '2025-05-27T12:12:17.563Z',
number: 1,
};

const mockMigrationStatsAborted = {
status: SiemMigrationTaskStatus.ABORTED,
id: 'c44d2c7d-0de1-4231-8b82-0dcfd67a9fe3',
rules: { total: 6, pending: 6, processing: 0, completed: 0, failed: 0 },
created_at: '2025-05-27T12:12:17.563Z',
last_updated_at: '2025-05-27T12:12:17.563Z',
number: 1,
};

const mockMigrationStatsReady = {
status: SiemMigrationTaskStatus.READY,
id: 'c44d2c7d-0de1-4231-8b82-0dcfd67a9fe3',
rules: { total: 6, pending: 6, processing: 0, completed: 0, failed: 0 },
created_at: '2025-05-27T12:12:17.563Z',
last_updated_at: '2025-05-27T12:12:17.563Z',
number: 1,
};

const missingMacro: RuleMigrationResourceBase = {
type: 'macro',
name: 'macro1',
};
const missingLookup: RuleMigrationResourceBase = {
type: 'lookup',
name: 'lookup1',
};

jest.mock('../../service/hooks/use_get_missing_resources');
const useGetMissingResourcesMock = useGetMissingResources as jest.Mock;

describe('MigrationReadyPanel', () => {
beforeEach(() => {
useGetMissingResourcesMock.mockReturnValue({
getMissingResources: jest.fn().mockResolvedValue([]),
isLoading: false,
});

useStartMigrationMock.mockReturnValue({
startMigration: mockStartMigration,
isLoading: false,
error: null,
});
});

describe('Ready Migration', () => {
it('should render description text correctly', () => {
render(<MigrationReadyPanel migrationStats={mockMigrationStatsReady} />);
expect(screen.getByTestId('ruleMigrationDescription')).toHaveTextContent(
`Migration of 6 rules is created but the translation has not started yet.`
);
});

it('should render start migration button', () => {
render(<MigrationReadyPanel migrationStats={mockMigrationStatsReady} />);
expect(screen.getByTestId('startMigrationButton')).toBeVisible();
expect(screen.getByTestId('startMigrationButton')).toHaveTextContent('Start translation');
});
});

describe('Migration with Error', () => {
it('should render error message when migration has an error', () => {
render(<MigrationReadyPanel migrationStats={mockMigrationStateWithError} />);
expect(screen.getByTestId('ruleMigrationDescription')).toHaveTextContent(
'Migration of 6 rules failed. Please correct the below error and try again.'
);
expect(screen.getByTestId('ruleMigrationLastError')).toHaveTextContent(
'Failed to populate ELSER indices. Make sure the ELSER model is deployed and running at Machine Learning > Trained Models. Error: Exception when running inference id [.elser-2-elasticsearch] on field [elser_embedding]'
);
});

it('should render start migration button when there is an error', () => {
render(<MigrationReadyPanel migrationStats={mockMigrationStateWithError} />);
expect(screen.queryByTestId('startMigrationButton')).toHaveTextContent('Start translation');
});
});

describe('Aborted Migration', () => {
it('should render aborted migration message', () => {
render(<MigrationReadyPanel migrationStats={mockMigrationStatsAborted} />);
expect(screen.getByTestId('ruleMigrationDescription')).toHaveTextContent(
'Migration of 6 rules was stopped. You can resume it any time.'
);
});

it('should render correct start migration button for aborted migration', () => {
render(<MigrationReadyPanel migrationStats={mockMigrationStatsAborted} />);
expect(screen.getByTestId('startMigrationButton')).toHaveTextContent('Resume translation');
});
});

describe('Missing Resources', () => {
const missingResources = [missingMacro, missingLookup];

beforeEach(() => {
useGetMissingResourcesMock.mockImplementation((setterFn: Function) => {
return {
getMissingResources: jest.fn().mockImplementation(() => {
setterFn(missingResources);
}),
isLoading: false,
};
});
});

it('should render missing resources warning when there are missing resources', async () => {
render(<MigrationReadyPanel migrationStats={mockMigrationStatsReady} />);
await waitFor(() => {
expect(screen.getByTestId('ruleMigrationDescription')).toHaveTextContent(
'Migration of 6 rules is created but the translation has not started yet. Upload macros & lookups and start the translation process.'
);
});
});

it('should render missing resources button', async () => {
render(<MigrationReadyPanel migrationStats={mockMigrationStatsReady} />);
expect(screen.getByTestId('ruleMigrationMissingResourcesButton')).toBeVisible();
});
});
});
Loading