mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
## 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:
parent
751de263cf
commit
aa012d6761
20 changed files with 578 additions and 55 deletions
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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 } : {}),
|
||||
};
|
||||
};
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -13,8 +13,3 @@ export interface RuleMigrationStats extends RuleMigrationTaskStats {
|
|||
/** The sequential number of the migration */
|
||||
number: number;
|
||||
}
|
||||
|
||||
export interface RetryRuleMigrationFilter {
|
||||
failed?: boolean;
|
||||
notFullyTranslated?: boolean;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 } });
|
||||
|
|
|
@ -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 } };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() } };
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue