mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
## 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:
parent
83651607ee
commit
e4586dac80
28 changed files with 527 additions and 123 deletions
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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%',
|
||||
|
|
|
@ -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 = '-';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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%',
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,3 +13,8 @@ export interface RuleMigrationStats extends RuleMigrationTaskStats {
|
|||
/** The sequential number of the migration */
|
||||
number: number;
|
||||
}
|
||||
|
||||
export interface RetryRuleMigrationFilter {
|
||||
failed?: boolean;
|
||||
notFullyTranslated?: boolean;
|
||||
}
|
||||
|
|
|
@ -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 } });
|
||||
}
|
||||
|
|
|
@ -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 } };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 } };
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue