Skip to content

Commit

Permalink
[Rules migration] Add migration rules filters (elastic#11386) (elasti…
Browse files Browse the repository at this point in the history
…c#206089)

## Summary

[Internal link](elastic/security-team#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"
/>
  • Loading branch information
e40pud authored and delanni committed Jan 13, 2025
1 parent 51b86ac commit 8e49a4e
Show file tree
Hide file tree
Showing 20 changed files with 575 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>;

Expand Down Expand Up @@ -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>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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>(
Expand All @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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';
Loading

0 comments on commit 8e49a4e

Please sign in to comment.