[Rules migration] Add migration rules filters (#11386) (#206089)

## Summary

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

These changes add a migration rules filtering functionality and allows
user to filter rules by `Status` and `Author`.

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

## Screenshots

### Filter by `Status`

<img width="1775" alt="Screenshot 2025-01-09 at 15 38 28"
src="https://github.com/user-attachments/assets/02f2a916-e0a1-4741-a602-50a032600c39"
/>

### Filter by `Author`

<img width="1774" alt="Screenshot 2025-01-09 at 15 38 38"
src="https://github.com/user-attachments/assets/4a44af77-4665-4c7c-86c4-c9f08918ea1f"
/>
This commit is contained in:
Ievgen Sorokopud 2025-01-13 15:53:16 +01:00 committed by GitHub
parent 751de263cf
commit aa012d6761
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 578 additions and 55 deletions

View file

@ -46,6 +46,11 @@ export enum SiemMigrationStatus {
FAILED = 'failed',
}
export enum SiemMigrationRetryFilter {
FAILED = 'failed',
NOT_FULLY_TRANSLATED = 'not_fully_translated',
}
export enum RuleTranslationResult {
FULL = 'full',
PARTIAL = 'partial',
@ -62,3 +67,16 @@ export const DEFAULT_TRANSLATION_FIELDS = {
to: 'now',
interval: '5m',
} as const;
export enum AuthorFilter {
ELASTIC = 'elastic',
CUSTOM = 'custom',
}
export enum StatusFilter {
INSTALLED = 'installed',
TRANSLATED = 'translated',
PARTIALLY_TRANSLATED = 'partially_translated',
UNTRANSLATABLE = 'untranslatable',
FAILED = 'failed',
}

View file

@ -15,13 +15,14 @@
*/
import { z } from '@kbn/zod';
import { ArrayFromString } from '@kbn/zod-helpers';
import { ArrayFromString, BooleanFromString } from '@kbn/zod-helpers';
import {
UpdateRuleMigrationData,
RuleMigrationTaskStats,
OriginalRule,
RuleMigration,
RuleMigrationRetryFilter,
RuleMigrationTranslationStats,
PrebuiltRuleVersion,
RuleMigrationResourceData,
@ -63,6 +64,12 @@ export const GetRuleMigrationRequestQuery = z.object({
sort_direction: z.enum(['asc', 'desc']).optional(),
search_term: z.string().optional(),
ids: ArrayFromString(NonEmptyString).optional(),
is_prebuilt: BooleanFromString.optional(),
is_installed: BooleanFromString.optional(),
is_fully_translated: BooleanFromString.optional(),
is_partially_translated: BooleanFromString.optional(),
is_untranslatable: BooleanFromString.optional(),
is_failed: BooleanFromString.optional(),
});
export type GetRuleMigrationRequestQueryInput = z.input<typeof GetRuleMigrationRequestQuery>;
@ -234,14 +241,7 @@ export type RetryRuleMigrationRequestBody = z.infer<typeof RetryRuleMigrationReq
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(),
filter: RuleMigrationRetryFilter.optional(),
});
export type RetryRuleMigrationRequestBodyInput = z.input<typeof RetryRuleMigrationRequestBody>;

View file

@ -164,6 +164,36 @@ paths:
items:
description: The rule migration id
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
- name: is_prebuilt
in: query
required: false
schema:
type: boolean
- name: is_installed
in: query
required: false
schema:
type: boolean
- name: is_fully_translated
in: query
required: false
schema:
type: boolean
- name: is_partially_translated
in: query
required: false
schema:
type: boolean
- name: is_untranslatable
in: query
required: false
schema:
type: boolean
- name: is_failed
in: query
required: false
schema:
type: boolean
responses:
200:
@ -335,12 +365,8 @@ paths:
$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
filter:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationRetryFilter'
responses:
200:
description: Indicates the migration retry request has been processed successfully.

View file

@ -352,6 +352,14 @@ export const UpdateRuleMigrationData = z.object({
comments: RuleMigrationComments.optional(),
});
/**
* Indicates the filter to retry the migrations rules translation
*/
export type RuleMigrationRetryFilter = z.infer<typeof RuleMigrationRetryFilter>;
export const RuleMigrationRetryFilter = z.enum(['failed', 'not_fully_translated']);
export type RuleMigrationRetryFilterEnum = typeof RuleMigrationRetryFilter.enum;
export const RuleMigrationRetryFilterEnum = RuleMigrationRetryFilter.enum;
/**
* The type of the rule migration resource.
*/

View file

@ -320,6 +320,13 @@ components:
description: The comments for the migration including a summary from the LLM in markdown.
$ref: '#/components/schemas/RuleMigrationComments'
RuleMigrationRetryFilter:
type: string
description: Indicates the filter to retry the migrations rules translation
enum: # should match SiemMigrationRetryFilter enum at ../constants.ts
- failed
- not_fully_translated
## Rule migration resources
RuleMigrationResourceType:

View file

@ -11,6 +11,7 @@ import type { UpdateRuleMigrationData } from '../../../../common/siem_migrations
import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen';
import { KibanaServices } from '../../../common/lib/kibana';
import type { SiemMigrationRetryFilter } from '../../../../common/siem_migrations/constants';
import {
SIEM_RULE_MIGRATIONS_PATH,
SIEM_RULE_MIGRATIONS_ALL_STATS_PATH,
@ -170,10 +171,8 @@ export interface RetryRuleMigrationParams {
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 indicator to filter migration rules to retry */
filter?: SiemMigrationRetryFilter;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}
@ -182,14 +181,12 @@ export const retryRuleMigration = async ({
migrationId,
connectorId,
langSmithOptions,
failed,
notFullyTranslated,
filter,
signal,
}: RetryRuleMigrationParams): Promise<RetryRuleMigrationResponse> => {
const body: RetryRuleMigrationRequestBody = {
connector_id: connectorId,
failed,
not_fully_translated: notFullyTranslated,
filter,
};
if (langSmithOptions) {
body.langsmith_options = langSmithOptions;
@ -215,6 +212,18 @@ export interface GetRuleMigrationParams {
searchTerm?: string;
/** Optional rules ids to filter documents */
ids?: string[];
/** Optional attribute to retrieve prebuilt migration rules */
isPrebuilt?: boolean;
/** Optional attribute to retrieve installed migration rules */
isInstalled?: boolean;
/** Optional attribute to retrieve fully translated migration rules */
isFullyTranslated?: boolean;
/** Optional attribute to retrieve partially translated migration rules */
isPartiallyTranslated?: boolean;
/** Optional attribute to retrieve untranslated migration rules */
isUntranslatable?: boolean;
/** Optional attribute to retrieve failed migration rules */
isFailed?: boolean;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}
@ -227,6 +236,12 @@ export const getRuleMigrations = async ({
sortDirection,
searchTerm,
ids,
isPrebuilt,
isInstalled,
isFullyTranslated,
isPartiallyTranslated,
isUntranslatable,
isFailed,
signal,
}: GetRuleMigrationParams): Promise<GetRuleMigrationResponse> => {
return KibanaServices.get().http.get<GetRuleMigrationResponse>(
@ -240,6 +255,12 @@ export const getRuleMigrations = async ({
sort_direction: sortDirection,
search_term: searchTerm,
ids,
is_prebuilt: isPrebuilt,
is_installed: isInstalled,
is_fully_translated: isFullyTranslated,
is_partially_translated: isPartiallyTranslated,
is_untranslatable: isUntranslatable,
is_failed: isFailed,
},
signal,
}

View file

@ -0,0 +1,98 @@
/*
* 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, { useCallback, useMemo, useState } from 'react';
import type { EuiSelectableOption } from '@elastic/eui';
import { EuiFilterButton, EuiPopover, EuiSelectable } from '@elastic/eui';
import type { EuiSelectableOnChangeEvent } from '@elastic/eui/src/components/selectable/selectable';
import { AuthorFilter } from '../../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
const AUTHOR_FILTER_POPOVER_WIDTH = 150;
export interface AuthorFilterButtonProps {
author?: AuthorFilter;
onAuthorChanged: (newAuthor?: AuthorFilter) => void;
}
export const AuthorFilterButton: React.FC<AuthorFilterButtonProps> = React.memo(
({ author, onAuthorChanged }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const selectableOptions: EuiSelectableOption[] = useMemo(
() => [
{
label: i18n.ELASTIC_AUTHOR_FILTER_OPTION,
data: { author: AuthorFilter.ELASTIC },
checked: author === AuthorFilter.ELASTIC ? 'on' : undefined,
},
{
label: i18n.CUSTOM_AUTHOR_FILTER_OPTION,
data: { author: AuthorFilter.CUSTOM },
checked: author === AuthorFilter.CUSTOM ? 'on' : undefined,
},
],
[author]
);
const handleOptionsChange = useCallback(
(
_options: EuiSelectableOption[],
_event: EuiSelectableOnChangeEvent,
changedOption: EuiSelectableOption
) => {
setIsPopoverOpen(false);
if (changedOption.checked && changedOption?.data?.author) {
onAuthorChanged(changedOption.data.author);
} else if (!changedOption.checked) {
onAuthorChanged();
}
},
[onAuthorChanged]
);
const triggerButton = (
<EuiFilterButton
grow
iconType="arrowDown"
onClick={() => {
setIsPopoverOpen(!isPopoverOpen);
}}
isSelected={isPopoverOpen}
hasActiveFilters={author !== undefined}
data-test-subj="authorFilterButton"
>
{i18n.AUTHOR_BUTTON_TITLE}
</EuiFilterButton>
);
return (
<EuiPopover
ownFocus
button={triggerButton}
isOpen={isPopoverOpen}
closePopover={() => {
setIsPopoverOpen(!isPopoverOpen);
}}
panelPaddingSize="none"
repositionOnScroll
>
<EuiSelectable
aria-label={i18n.AUTHOR_FILTER_ARIAL_LABEL}
options={selectableOptions}
onChange={handleOptionsChange}
singleSelection
data-test-subj="authorFilterSelectableList"
>
{(list) => <div style={{ width: AUTHOR_FILTER_POPOVER_WIDTH }}>{list}</div>}
</EuiSelectable>
</EuiPopover>
);
}
);
AuthorFilterButton.displayName = 'AuthorFilterButton';

View file

@ -0,0 +1,57 @@
/*
* 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, { useCallback } from 'react';
import { EuiFilterGroup } from '@elastic/eui';
import type {
AuthorFilter,
StatusFilter,
} from '../../../../../../common/siem_migrations/constants';
import { StatusFilterButton } from './status';
import { AuthorFilterButton } from './author';
export interface FilterOptions {
status?: StatusFilter;
author?: AuthorFilter;
}
export interface MigrationRulesFilterProps {
filterOptions?: FilterOptions;
onFilterOptionsChanged: (filterOptions?: FilterOptions) => void;
}
export const MigrationRulesFilter: React.FC<MigrationRulesFilterProps> = React.memo(
({ filterOptions, onFilterOptionsChanged }) => {
const handleOnStatusChanged = useCallback(
(newStatus?: StatusFilter) => {
onFilterOptionsChanged({ ...filterOptions, ...{ status: newStatus } });
},
[filterOptions, onFilterOptionsChanged]
);
const handleOnAuthorChanged = useCallback(
(newAuthor?: AuthorFilter) => {
onFilterOptionsChanged({ ...filterOptions, ...{ author: newAuthor } });
},
[filterOptions, onFilterOptionsChanged]
);
return (
<EuiFilterGroup>
<StatusFilterButton
status={filterOptions?.status}
onStatusChanged={handleOnStatusChanged}
/>
<AuthorFilterButton
author={filterOptions?.author}
onAuthorChanged={handleOnAuthorChanged}
/>
</EuiFilterGroup>
);
}
);
MigrationRulesFilter.displayName = 'MigrationRulesFilter';

View file

@ -0,0 +1,114 @@
/*
* 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, { useCallback, useState } from 'react';
import type { EuiSelectableOption } from '@elastic/eui';
import { EuiFilterButton, EuiPopover, EuiSelectable } from '@elastic/eui';
import type { EuiSelectableOnChangeEvent } from '@elastic/eui/src/components/selectable/selectable';
import {
RuleTranslationResult,
StatusFilter,
} from '../../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
import { convertTranslationResultIntoText } from '../../../utils/translation_results';
const STATUS_FILTER_POPOVER_WIDTH = 250;
export interface StatusFilterButtonProps {
status?: StatusFilter;
onStatusChanged: (newStatus?: StatusFilter) => void;
}
export const StatusFilterButton: React.FC<StatusFilterButtonProps> = React.memo(
({ status, onStatusChanged }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const selectableOptions: EuiSelectableOption[] = [
{
label: i18n.INSTALL_FILTER_OPTION,
data: { status: StatusFilter.INSTALLED },
checked: status === StatusFilter.INSTALLED ? 'on' : undefined,
},
{
label: convertTranslationResultIntoText(RuleTranslationResult.FULL),
data: { status: StatusFilter.TRANSLATED },
checked: status === StatusFilter.TRANSLATED ? 'on' : undefined,
},
{
label: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL),
data: { status: StatusFilter.PARTIALLY_TRANSLATED },
checked: status === StatusFilter.PARTIALLY_TRANSLATED ? 'on' : undefined,
},
{
label: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE),
data: { status: StatusFilter.UNTRANSLATABLE },
checked: status === StatusFilter.UNTRANSLATABLE ? 'on' : undefined,
},
{
label: i18n.FAILED_FILTER_OPTION,
data: { status: StatusFilter.FAILED },
checked: status === StatusFilter.FAILED ? 'on' : undefined,
},
];
const handleOptionsChange = useCallback(
(
_options: EuiSelectableOption[],
_event: EuiSelectableOnChangeEvent,
changedOption: EuiSelectableOption
) => {
setIsPopoverOpen(false);
if (changedOption.checked && changedOption?.data?.status) {
onStatusChanged(changedOption.data.status);
} else if (!changedOption.checked) {
onStatusChanged();
}
},
[onStatusChanged]
);
const triggerButton = (
<EuiFilterButton
grow
iconType="arrowDown"
onClick={() => {
setIsPopoverOpen(!isPopoverOpen);
}}
isSelected={isPopoverOpen}
hasActiveFilters={status !== undefined}
data-test-subj="statusFilterButton"
>
{i18n.STATUS_BUTTON_TITLE}
</EuiFilterButton>
);
return (
<EuiPopover
ownFocus
button={triggerButton}
isOpen={isPopoverOpen}
closePopover={() => {
setIsPopoverOpen(!isPopoverOpen);
}}
panelPaddingSize="none"
repositionOnScroll
>
<EuiSelectable
aria-label={i18n.STATUS_FILTER_ARIAL_LABEL}
options={selectableOptions}
onChange={handleOptionsChange}
singleSelection
data-test-subj="statusFilterSelectableList"
>
{(list) => <div style={{ width: STATUS_FILTER_POPOVER_WIDTH }}>{list}</div>}
</EuiSelectable>
</EuiPopover>
);
}
);
StatusFilterButton.displayName = 'StatusFilterButton';

View file

@ -0,0 +1,64 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const STATUS_BUTTON_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.filters.statusButtonTitle',
{
defaultMessage: 'Status',
}
);
export const STATUS_FILTER_ARIAL_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.filters.statusArialLabel',
{
defaultMessage: 'Status filter',
}
);
export const INSTALL_FILTER_OPTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.filters.statusInstallOption',
{
defaultMessage: 'Install',
}
);
export const FAILED_FILTER_OPTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.filters.statusFailedOption',
{
defaultMessage: 'Failed',
}
);
export const AUTHOR_BUTTON_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.filters.authorButtonTitle',
{
defaultMessage: 'Author',
}
);
export const AUTHOR_FILTER_ARIAL_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.filters.authorArialLabel',
{
defaultMessage: 'Author filter',
}
);
export const ELASTIC_AUTHOR_FILTER_OPTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.filters.authorElasticOption',
{
defaultMessage: 'Elastic',
}
);
export const CUSTOM_AUTHOR_FILTER_OPTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.filters.authorCustomOption',
{
defaultMessage: 'Custom',
}
);

View file

@ -0,0 +1,25 @@
/*
* 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 { AuthorFilter, StatusFilter } from '../../../../../common/siem_migrations/constants';
import type { FilterOptions } from './filters';
export const convertFilterOptions = (filterOptions?: FilterOptions) => {
return {
...(filterOptions?.author === AuthorFilter.ELASTIC ? { isPrebuilt: true } : {}),
...(filterOptions?.author === AuthorFilter.CUSTOM ? { isPrebuilt: false } : {}),
...(filterOptions?.status === StatusFilter.FAILED ? { isFailed: true } : {}),
...(filterOptions?.status === StatusFilter.INSTALLED ? { isInstalled: true } : {}),
...(filterOptions?.status === StatusFilter.TRANSLATED
? { isInstalled: false, isFullyTranslated: true }
: {}),
...(filterOptions?.status === StatusFilter.PARTIALLY_TRANSLATED
? { isPartiallyTranslated: true }
: {}),
...(filterOptions?.status === StatusFilter.UNTRANSLATABLE ? { isUntranslatable: true } : {}),
};
};

View file

@ -32,9 +32,15 @@ import { useGetMigrationPrebuiltRules } from '../../logic/use_get_migration_preb
import * as logicI18n from '../../logic/translations';
import { BulkActions } from './bulk_actions';
import { SearchField } from './search_field';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import {
RuleTranslationResult,
SiemMigrationRetryFilter,
} from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
import { useRetryRuleMigration } from '../../service/hooks/use_retry_rules';
import type { FilterOptions } from './filters';
import { MigrationRulesFilter } from './filters';
import { convertFilterOptions } from './helpers';
const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_SORT_FIELD = 'translation_result';
@ -75,6 +81,9 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION);
const [searchTerm, setSearchTerm] = useState<string | undefined>();
// Filters
const [filterOptions, setFilterOptions] = useState<FilterOptions | undefined>();
const { data: translationStats, isLoading: isStatsLoading } =
useGetMigrationTranslationStats(migrationId);
@ -91,6 +100,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
sortField,
sortDirection,
searchTerm,
...convertFilterOptions(filterOptions),
});
const [selectedRuleMigrations, setSelectedRuleMigrations] = useState<RuleMigration[]>([]);
@ -199,7 +209,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
);
const reprocessFailedRules = useCallback(async () => {
retryRuleMigration(migrationId, { failed: true });
retryRuleMigration(migrationId, SiemMigrationRetryFilter.FAILED);
}, [migrationId, retryRuleMigration]);
const isLoading =
@ -310,6 +320,12 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
<EuiFlexItem>
<SearchField initialValue={searchTerm} onSearch={handleOnSearch} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<MigrationRulesFilter
filterOptions={filterOptions}
onFilterOptionsChanged={setFilterOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<BulkActions
isTableLoading={isLoading}

View file

@ -22,6 +22,12 @@ export const useGetMigrationRules = (params: {
sortDirection?: 'asc' | 'desc';
searchTerm?: string;
ids?: string[];
isPrebuilt?: boolean;
isInstalled?: boolean;
isFullyTranslated?: boolean;
isPartiallyTranslated?: boolean;
isUntranslatable?: boolean;
isFailed?: boolean;
}) => {
const { addError } = useAppToasts();

View file

@ -9,7 +9,7 @@ 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';
import type { SiemMigrationRetryFilter } from '../../../../../common/siem_migrations/constants';
export const RETRY_RULE_MIGRATION_SUCCESS = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.service.retryMigrationRulesSuccess',
@ -20,7 +20,7 @@ export const RETRY_RULE_MIGRATION_ERROR = i18n.translate(
{ defaultMessage: 'Error retrying a rule migration.' }
);
export type RetryRuleMigration = (migrationId: string, filter?: RetryRuleMigrationFilter) => void;
export type RetryRuleMigration = (migrationId: string, filter?: SiemMigrationRetryFilter) => void;
export type OnSuccess = () => void;
export const useRetryRuleMigration = (onSuccess?: OnSuccess) => {

View file

@ -25,6 +25,7 @@ import type {
StartRuleMigrationResponse,
UpsertRuleMigrationResourcesRequestBody,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SiemMigrationRetryFilter } from '../../../../common/siem_migrations/constants';
import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants';
import type { StartPluginsDependencies } from '../../../types';
import { ExperimentalFeaturesService } from '../../../common/experimental_features_service';
@ -40,7 +41,7 @@ import {
retryRuleMigration,
getIntegrations,
} from '../api';
import type { RetryRuleMigrationFilter, RuleMigrationStats } from '../types';
import type { RuleMigrationStats } from '../types';
import { getSuccessToast } from './success_notification';
import { RuleMigrationsStorage } from './storage';
import * as i18n from './translations';
@ -145,7 +146,7 @@ export class SiemRulesMigrationsService {
public async retryRuleMigration(
migrationId: string,
filter?: RetryRuleMigrationFilter
filter?: SiemMigrationRetryFilter
): Promise<RetryRuleMigrationResponse> {
const connectorId = this.connectorIdStorage.get();
if (!connectorId) {
@ -165,7 +166,7 @@ export class SiemRulesMigrationsService {
migrationId,
connectorId,
langSmithOptions,
...filter,
filter,
});
this.startPolling();
return result;

View file

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

View file

@ -46,13 +46,28 @@ export const registerSiemRuleMigrationsGetRoute = (
sort_direction: sortDirection,
search_term: searchTerm,
ids,
is_prebuilt: isPrebuilt,
is_installed: isInstalled,
is_fully_translated: isFullyTranslated,
is_partially_translated: isPartiallyTranslated,
is_untranslatable: isUntranslatable,
is_failed: isFailed,
} = req.query;
try {
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const options: RuleMigrationGetOptions = {
filters: { searchTerm, ids },
filters: {
searchTerm,
ids,
prebuilt: isPrebuilt,
installed: isInstalled,
fullyTranslated: isFullyTranslated,
partiallyTranslated: isPartiallyTranslated,
untranslatable: isUntranslatable,
failed: isFailed,
},
sort: { sortField, sortDirection },
size: perPage,
from: page && perPage ? page * perPage : 0,

View file

@ -45,8 +45,7 @@ export const registerSiemRuleMigrationsRetryRoute = (
const {
langsmith_options: langsmithOptions,
connector_id: connectorId,
failed,
not_fully_translated: notFullyTranslated,
filter,
} = req.body;
try {
@ -65,7 +64,10 @@ export const registerSiemRuleMigrationsRetryRoute = (
],
};
const filters: RuleMigrationFilters = { failed, notFullyTranslated };
const filters: RuleMigrationFilters = {
...(filter === 'failed' ? { failed: true } : {}),
...(filter === 'not_fully_translated' ? { fullyTranslated: false } : {}),
};
const { updated } = await ruleMigrationsClient.task.updateToRetry(migrationId, filters);
if (!updated) {
return res.ok({ body: { started: false } });

View file

@ -41,11 +41,13 @@ export type RuleMigrationAllDataStats = RuleMigrationDataStats[];
export interface RuleMigrationFilters {
status?: SiemMigrationStatus | SiemMigrationStatus[];
ids?: string[];
installed?: boolean;
installable?: boolean;
prebuilt?: boolean;
custom?: boolean;
failed?: boolean;
notFullyTranslated?: boolean;
fullyTranslated?: boolean;
partiallyTranslated?: boolean;
untranslatable?: boolean;
searchTerm?: string;
}
export interface RuleMigrationGetOptions {
@ -408,12 +410,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
{
status,
ids,
installed,
installable,
prebuilt,
custom,
searchTerm,
failed,
notFullyTranslated,
fullyTranslated,
partiallyTranslated,
untranslatable,
}: RuleMigrationFilters = {}
): QueryDslQueryContainer {
const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }];
@ -427,24 +431,44 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
if (ids) {
filter.push({ terms: { _id: ids } });
}
if (installable) {
filter.push(...searchConditions.isInstallable());
}
if (prebuilt) {
filter.push(searchConditions.isPrebuilt());
}
if (custom) {
filter.push(searchConditions.isCustom());
}
if (searchTerm?.length) {
filter.push(searchConditions.matchTitle(searchTerm));
}
if (failed) {
filter.push(searchConditions.isFailed());
if (installed === true) {
filter.push(searchConditions.isInstalled());
} else if (installed === false) {
filter.push(searchConditions.isNotInstalled());
}
if (notFullyTranslated) {
if (installable === true) {
filter.push(...searchConditions.isInstallable());
} else if (installable === false) {
filter.push(...searchConditions.isNotInstallable());
}
if (prebuilt === true) {
filter.push(searchConditions.isPrebuilt());
} else if (prebuilt === false) {
filter.push(searchConditions.isCustom());
}
if (failed === true) {
filter.push(searchConditions.isFailed());
} else if (failed === false) {
filter.push(searchConditions.isNotFailed());
}
if (fullyTranslated === true) {
filter.push(searchConditions.isFullyTranslated());
} else if (fullyTranslated === false) {
filter.push(searchConditions.isNotFullyTranslated());
}
if (partiallyTranslated === true) {
filter.push(searchConditions.isPartiallyTranslated());
} else if (partiallyTranslated === false) {
filter.push(searchConditions.isNotPartiallyTranslated());
}
if (untranslatable === true) {
filter.push(searchConditions.isUntranslatable());
} else if (untranslatable === false) {
filter.push(searchConditions.isNotUntranslatable());
}
return { bool: { filter } };
}
}

View file

@ -18,6 +18,26 @@ export const conditions = {
isNotFullyTranslated(): QueryDslQueryContainer {
return { bool: { must_not: conditions.isFullyTranslated() } };
},
isPartiallyTranslated(): QueryDslQueryContainer {
return { term: { translation_result: RuleTranslationResult.PARTIAL } };
},
isNotPartiallyTranslated(): QueryDslQueryContainer {
return { bool: { must_not: conditions.isPartiallyTranslated() } };
},
isUntranslatable(): QueryDslQueryContainer {
return { term: { translation_result: RuleTranslationResult.UNTRANSLATABLE } };
},
isNotUntranslatable(): QueryDslQueryContainer {
return { bool: { must_not: conditions.isUntranslatable() } };
},
isInstalled(): QueryDslQueryContainer {
return {
nested: {
path: 'elastic_rule',
query: { exists: { field: 'elastic_rule.id' } },
},
};
},
isNotInstalled(): QueryDslQueryContainer {
return {
nested: {
@ -53,7 +73,13 @@ export const conditions = {
isInstallable(): QueryDslQueryContainer[] {
return [this.isFullyTranslated(), this.isNotInstalled()];
},
isNotInstallable(): QueryDslQueryContainer[] {
return [this.isNotFullyTranslated(), this.isInstalled()];
},
isFailed(): QueryDslQueryContainer {
return { term: { status: SiemMigrationStatus.FAILED } };
},
isNotFailed(): QueryDslQueryContainer {
return { bool: { must_not: conditions.isFailed() } };
},
};