[Rules migration] Add pagination functionality to rules migration table (#11313) (#202494)

## Summary

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

With these changes we add pagination functionality for the rules
migration table. This way we will improve the performance within the
page.

Also, added as part of these PR:
* moved `install` and `install_translated` routes to the `rules/api`
folder; before those were located in `rules/api/rules` and made
confusion
* a new `translation_stats` route to return stats for the specific
migration about the translated rules, like `total` number of the rules,
and number of `prebuilt`, `custom` and `installable` rules
* add `Updated` table column
* small UI fixes:
  * use correct icon for "SIEM rule migration"
* do not remove "Install translated rules" button and rather disable it
when there are no installable rules
  * do not allow user to update translation status via UI

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ievgen Sorokopud 2024-12-03 18:50:10 +01:00 committed by GitHub
parent 6e5fc696a6
commit a662233d8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 838 additions and 234 deletions

View file

@ -352,6 +352,7 @@ import type {
CreateRuleMigrationRequestBodyInput,
CreateRuleMigrationResponse,
GetAllStatsRuleMigrationResponse,
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
GetRuleMigrationResponse,
GetRuleMigrationResourcesRequestQueryInput,
@ -359,6 +360,8 @@ import type {
GetRuleMigrationResourcesResponse,
GetRuleMigrationStatsRequestParamsInput,
GetRuleMigrationStatsResponse,
GetRuleMigrationTranslationStatsRequestParamsInput,
GetRuleMigrationTranslationStatsResponse,
InstallMigrationRulesRequestParamsInput,
InstallMigrationRulesRequestBodyInput,
InstallMigrationRulesResponse,
@ -1415,6 +1418,8 @@ finalize it.
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'GET',
query: props.query,
})
.catch(catchAxiosErrorFormatAndThrow);
}
@ -1453,6 +1458,24 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retrieves the translation stats of a SIEM rules migration using the migration id provided
*/
async getRuleMigrationTranslationStats(props: GetRuleMigrationTranslationStatsProps) {
this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationTranslationStats`);
return this.kbnClient
.request<GetRuleMigrationTranslationStatsResponse>({
path: replaceParams(
'/internal/siem_migrations/rules/{migration_id}/translation_stats',
props.params
),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'GET',
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Get the details of an existing saved Timeline or Timeline template.
*/
@ -2334,6 +2357,7 @@ export interface GetRuleExecutionResultsProps {
params: GetRuleExecutionResultsRequestParamsInput;
}
export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput;
}
export interface GetRuleMigrationResourcesProps {
@ -2343,6 +2367,9 @@ export interface GetRuleMigrationResourcesProps {
export interface GetRuleMigrationStatsProps {
params: GetRuleMigrationStatsRequestParamsInput;
}
export interface GetRuleMigrationTranslationStatsProps {
params: GetRuleMigrationTranslationStatsRequestParamsInput;
}
export interface GetTimelineProps {
query: GetTimelineRequestQueryInput;
}

View file

@ -15,6 +15,8 @@ export const SIEM_RULE_MIGRATION_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/{migration
export const SIEM_RULE_MIGRATION_START_PATH = `${SIEM_RULE_MIGRATION_PATH}/start` as const;
export const SIEM_RULE_MIGRATION_RETRY_PATH = `${SIEM_RULE_MIGRATION_PATH}/retry` as const;
export const SIEM_RULE_MIGRATION_STATS_PATH = `${SIEM_RULE_MIGRATION_PATH}/stats` as const;
export const SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH =
`${SIEM_RULE_MIGRATION_PATH}/translation_stats` as const;
export const SIEM_RULE_MIGRATION_STOP_PATH = `${SIEM_RULE_MIGRATION_PATH}/stop` as const;
export const SIEM_RULE_MIGRATION_INSTALL_PATH = `${SIEM_RULE_MIGRATION_PATH}/install` as const;
export const SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH =

View file

@ -24,6 +24,7 @@ import {
RuleMigrationComments,
RuleMigrationTaskStats,
RuleMigration,
RuleMigrationTranslationStats,
RuleMigrationResourceData,
RuleMigrationResourceType,
RuleMigrationResource,
@ -44,6 +45,13 @@ export const CreateRuleMigrationResponse = z.object({
export type GetAllStatsRuleMigrationResponse = z.infer<typeof GetAllStatsRuleMigrationResponse>;
export const GetAllStatsRuleMigrationResponse = z.array(RuleMigrationTaskStats);
export type GetRuleMigrationRequestQuery = z.infer<typeof GetRuleMigrationRequestQuery>;
export const GetRuleMigrationRequestQuery = z.object({
page: z.coerce.number().optional(),
per_page: z.coerce.number().optional(),
search_term: z.string().optional(),
});
export type GetRuleMigrationRequestQueryInput = z.input<typeof GetRuleMigrationRequestQuery>;
export type GetRuleMigrationRequestParams = z.infer<typeof GetRuleMigrationRequestParams>;
export const GetRuleMigrationRequestParams = z.object({
@ -52,7 +60,13 @@ export const GetRuleMigrationRequestParams = z.object({
export type GetRuleMigrationRequestParamsInput = z.input<typeof GetRuleMigrationRequestParams>;
export type GetRuleMigrationResponse = z.infer<typeof GetRuleMigrationResponse>;
export const GetRuleMigrationResponse = z.array(RuleMigration);
export const GetRuleMigrationResponse = z.object({
/**
* The total number of rules in migration.
*/
total: z.number(),
data: z.array(RuleMigration),
});
export type GetRuleMigrationResourcesRequestQuery = z.infer<
typeof GetRuleMigrationResourcesRequestQuery
>;
@ -88,6 +102,21 @@ export type GetRuleMigrationStatsRequestParamsInput = z.input<
export type GetRuleMigrationStatsResponse = z.infer<typeof GetRuleMigrationStatsResponse>;
export const GetRuleMigrationStatsResponse = RuleMigrationTaskStats;
export type GetRuleMigrationTranslationStatsRequestParams = z.infer<
typeof GetRuleMigrationTranslationStatsRequestParams
>;
export const GetRuleMigrationTranslationStatsRequestParams = z.object({
migration_id: NonEmptyString,
});
export type GetRuleMigrationTranslationStatsRequestParamsInput = z.input<
typeof GetRuleMigrationTranslationStatsRequestParams
>;
export type GetRuleMigrationTranslationStatsResponse = z.infer<
typeof GetRuleMigrationTranslationStatsResponse
>;
export const GetRuleMigrationTranslationStatsResponse = RuleMigrationTranslationStats;
export type InstallMigrationRulesRequestParams = z.infer<typeof InstallMigrationRulesRequestParams>;
export const InstallMigrationRulesRequestParams = z.object({
migration_id: NonEmptyString,

View file

@ -185,15 +185,40 @@ paths:
schema:
description: The migration id to start
$ref: '../../common.schema.yaml#/components/schemas/NonEmptyString'
- name: page
in: query
required: false
schema:
type: number
- name: per_page
in: query
required: false
schema:
type: number
- name: search_term
in: query
required: false
schema:
type: string
responses:
200:
description: Indicates rule migration have been retrieved correctly.
content:
application/json:
schema:
type: array
items:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration'
type: object
required:
- total
- data
properties:
total:
type: number
description: The total number of rules in migration.
data:
type: array
items:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration'
204:
description: Indicates the migration id was not found.
@ -256,7 +281,7 @@ paths:
in: path
required: true
schema:
description: The migration id to start
description: The migration id to fetch stats for
$ref: '../../common.schema.yaml#/components/schemas/NonEmptyString'
responses:
200:
@ -268,6 +293,31 @@ paths:
204:
description: Indicates the migration id was not found.
/internal/siem_migrations/rules/{migration_id}/translation_stats:
get:
summary: Gets a rule migration translation stats
operationId: GetRuleMigrationTranslationStats
x-codegen-enabled: true
description: Retrieves the translation stats of 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 fetch translation stats for
$ref: '../../common.schema.yaml#/components/schemas/NonEmptyString'
responses:
200:
description: Indicates the migration stats has been retrieved correctly.
content:
application/json:
schema:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTranslationStats'
204:
description: Indicates the migration id was not found.
/internal/siem_migrations/rules/{migration_id}/stop:
put:
summary: Stops an existing rule migration

View file

@ -242,6 +242,38 @@ export const RuleMigrationTaskStats = z.object({
last_updated_at: z.string(),
});
/**
* The rule migration translation stats object.
*/
export type RuleMigrationTranslationStats = z.infer<typeof RuleMigrationTranslationStats>;
export const RuleMigrationTranslationStats = z.object({
/**
* The migration id
*/
id: NonEmptyString,
/**
* The rules migration translation stats.
*/
rules: z.object({
/**
* The total number of rules to migrate.
*/
total: z.number().int(),
/**
* The number of rules that matched Elastic prebuilt rules.
*/
prebuilt: z.number().int(),
/**
* The number of rules that did not match Elastic prebuilt rules and will be installed as custom rules.
*/
custom: z.number().int(),
/**
* The number of rules that can be installed.
*/
installable: z.number().int(),
}),
});
/**
* The type of the rule migration resource.
*/

View file

@ -197,7 +197,39 @@ components:
- running
- stopped
- finished
RuleMigrationTranslationStats:
type: object
description: The rule migration translation stats object.
required:
- id
- rules
properties:
id:
description: The migration id
$ref: './common.schema.yaml#/components/schemas/NonEmptyString'
rules:
type: object
description: The rules migration translation stats.
required:
- total
- prebuilt
- custom
- installable
properties:
total:
type: integer
description: The total number of rules to migrate.
prebuilt:
type: integer
description: The number of rules that matched Elastic prebuilt rules.
custom:
type: integer
description: The number of rules that did not match Elastic prebuilt rules and will be installed as custom rules.
installable:
type: integer
description: The number of rules that can be installed.
RuleMigrationTranslationResult:
type: string
description: The rule translation result.

View file

@ -0,0 +1,39 @@
/*
* 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 type { SVGProps } from 'react';
import React from 'react';
export const SiemMigrationsIcon: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
fill="none"
viewBox="0 0 32 32"
{...props}
>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_2763_341531)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 12C15.4292 12 19.8479 16.3267 19.9962 21.7201L20 22V23H11V32H10C4.47715 32 0 27.5228 0 22C0 16.4772 4.47715 12 10 12ZM9 21L9.00005 14.0619C5.05371 14.554 2 17.9204 2 22C2 25.9928 4.92513 29.3024 8.74934 29.9028L9 29.9381V21ZM11 21L11.0009 14.062L11.258 14.0983C14.7544 14.6506 17.4976 17.4677 17.9381 21H11Z"
fill="#343741"
/>
<path d="M26 22C26 13.1634 18.8366 6 10 6V8C17.732 8 24 14.268 24 22H26Z" fill="#007871" />
<path
d="M32 22C32 9.84974 22.1503 0 10 0V2C21.0457 2 30 10.9543 30 22H32Z"
fill="#007871"
/>
</g>
<defs>
<clipPath id="clip0_2763_341531">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
</svg>
);

View file

@ -13,7 +13,7 @@ import {
} from '../../common/constants';
import { SIEM_MIGRATIONS_RULES } from '../app/translations';
import type { LinkItem } from '../common/links/types';
import { IconConsoleCloud } from '../common/icons/console_cloud';
import { SiemMigrationsIcon } from '../common/icons/siem_migrations';
export const siemMigrationsLinks: LinkItem = {
id: SecurityPageName.siemMigrationsRules,
@ -21,7 +21,7 @@ export const siemMigrationsLinks: LinkItem = {
description: i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRulesDescription', {
defaultMessage: 'SIEM Rules Migrations.',
}),
landingIcon: IconConsoleCloud,
landingIcon: SiemMigrationsIcon,
path: SIEM_MIGRATIONS_RULES_PATH,
capabilities: [`${SERVER_APP_ID}.show`],
skipUrlState: true,

View file

@ -15,10 +15,12 @@ import {
SIEM_RULE_MIGRATION_INSTALL_PATH,
SIEM_RULE_MIGRATION_PATH,
SIEM_RULE_MIGRATION_START_PATH,
SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
} from '../../../../common/siem_migrations/constants';
import type {
GetAllStatsRuleMigrationResponse,
GetRuleMigrationResponse,
GetRuleMigrationTranslationStatsResponse,
InstallTranslatedMigrationRulesResponse,
InstallMigrationRulesResponse,
StartRuleMigrationRequestBody,
@ -67,6 +69,31 @@ export const startRuleMigration = async ({
);
};
/**
* Retrieves the translation stats for the migraion.
*
* @param migrationId `id` of the migration to retrieve translation stats for
* @param signal AbortSignal for cancelling request
*
* @throws An error if response is not OK
*/
export const getRuleMigrationTranslationStats = async ({
migrationId,
signal,
}: {
migrationId: string;
signal: AbortSignal | undefined;
}): Promise<GetRuleMigrationTranslationStatsResponse> => {
return KibanaServices.get().http.fetch<GetRuleMigrationTranslationStatsResponse>(
replaceParams(SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, { migration_id: migrationId }),
{
method: 'GET',
version: '1',
signal,
}
);
};
/**
* Retrieves all the migration rule documents of a specific migration.
*
@ -77,14 +104,29 @@ export const startRuleMigration = async ({
*/
export const getRuleMigrations = async ({
migrationId,
page,
perPage,
searchTerm,
signal,
}: {
migrationId: string;
page?: number;
perPage?: number;
searchTerm?: string;
signal: AbortSignal | undefined;
}): Promise<GetRuleMigrationResponse> => {
return KibanaServices.get().http.fetch<GetRuleMigrationResponse>(
replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }),
{ method: 'GET', version: '1', signal }
{
method: 'GET',
version: '1',
query: {
page,
per_page: perPage,
search_term: searchTerm,
},
signal,
}
);
};

View file

@ -9,26 +9,46 @@ import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { replaceParams } from '@kbn/openapi-common/shared';
import { useCallback } from 'react';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { DEFAULT_QUERY_OPTIONS } from './constants';
import { getRuleMigrations } from '../api';
import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants';
interface UseGetMigrationRulesQueryProps {
migrationId: string;
page?: number;
perPage?: number;
searchTerm?: string;
}
export interface MigrationRulesQueryResponse {
ruleMigrations: RuleMigration[];
total: number;
}
export const useGetMigrationRulesQuery = (
migrationId: string,
options?: UseQueryOptions<GetRuleMigrationResponse>
queryArgs: UseGetMigrationRulesQueryProps,
queryOptions?: UseQueryOptions<
MigrationRulesQueryResponse,
Error,
MigrationRulesQueryResponse,
[...string[], UseGetMigrationRulesQueryProps]
>
) => {
const { migrationId } = queryArgs;
const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, {
migration_id: migrationId,
});
return useQuery<GetRuleMigrationResponse>(
['GET', SPECIFIC_MIGRATION_PATH],
return useQuery(
['GET', SPECIFIC_MIGRATION_PATH, queryArgs],
async ({ signal }) => {
return getRuleMigrations({ migrationId, signal });
const response = await getRuleMigrations({ signal, ...queryArgs });
return { ruleMigrations: response.data, total: response.total };
},
{
...DEFAULT_QUERY_OPTIONS,
...options,
...queryOptions,
}
);
};
@ -47,6 +67,10 @@ export const useInvalidateGetMigrationRulesQuery = (migrationId: string) => {
});
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',
});

View file

@ -0,0 +1,60 @@
/*
* 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 type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { replaceParams } from '@kbn/openapi-common/shared';
import { useCallback } from 'react';
import { DEFAULT_QUERY_OPTIONS } from './constants';
import { getRuleMigrationTranslationStats } from '../api';
import type { GetRuleMigrationTranslationStatsResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH } from '../../../../../common/siem_migrations/constants';
export const useGetMigrationTranslationStatsQuery = (
migrationId: string,
options?: UseQueryOptions<GetRuleMigrationTranslationStatsResponse>
) => {
const SPECIFIC_MIGRATION_TRANSLATION_PATH = replaceParams(
SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
{
migration_id: migrationId,
}
);
return useQuery<GetRuleMigrationTranslationStatsResponse>(
['GET', SPECIFIC_MIGRATION_TRANSLATION_PATH],
async ({ signal }) => {
return getRuleMigrationTranslationStats({ migrationId, signal });
},
{
...DEFAULT_QUERY_OPTIONS,
...options,
}
);
};
/**
* We should use this hook to invalidate the translation stats cache. For
* example, rule migrations mutations, like installing a rule, should lead to cache invalidation.
*
* @returns A translation stats cache invalidation callback
*/
export const useInvalidateGetMigrationTranslationStatsQuery = (migrationId: string) => {
const queryClient = useQueryClient();
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]);
};

View file

@ -10,6 +10,7 @@ import type { InstallMigrationRulesResponse } from '../../../../../common/siem_m
import { SIEM_RULE_MIGRATION_INSTALL_PATH } from '../../../../../common/siem_migrations/constants';
import { installMigrationRules } from '../api';
import { useInvalidateGetMigrationRulesQuery } from './use_get_migration_rules_query';
import { useInvalidateGetMigrationTranslationStatsQuery } from './use_get_migration_translation_stats_query';
export const INSTALL_MIGRATION_RULES_MUTATION_KEY = ['POST', SIEM_RULE_MIGRATION_INSTALL_PATH];
@ -18,6 +19,8 @@ export const useInstallMigrationRulesMutation = (
options?: UseMutationOptions<InstallMigrationRulesResponse, Error, string[]>
) => {
const invalidateGetRuleMigrationsQuery = useInvalidateGetMigrationRulesQuery(migrationId);
const invalidateGetMigrationTranslationStatsQuery =
useInvalidateGetMigrationTranslationStatsQuery(migrationId);
return useMutation<InstallMigrationRulesResponse, Error, string[]>(
(ids: string[]) => installMigrationRules({ migrationId, ids }),
@ -26,6 +29,7 @@ export const useInstallMigrationRulesMutation = (
mutationKey: INSTALL_MIGRATION_RULES_MUTATION_KEY,
onSettled: (...args) => {
invalidateGetRuleMigrationsQuery();
invalidateGetMigrationTranslationStatsQuery();
if (options?.onSettled) {
options.onSettled(...args);

View file

@ -10,17 +10,20 @@ import type { InstallTranslatedMigrationRulesResponse } from '../../../../../com
import { SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH } from '../../../../../common/siem_migrations/constants';
import { installTranslatedMigrationRules } from '../api';
import { useInvalidateGetMigrationRulesQuery } from './use_get_migration_rules_query';
import { useInvalidateGetMigrationTranslationStatsQuery } from './use_get_migration_translation_stats_query';
export const INSTALL_ALL_MIGRATION_RULES_MUTATION_KEY = [
'POST',
SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH,
];
export const useInstallAllMigrationRulesMutation = (
export const useInstallTranslatedMigrationRulesMutation = (
migrationId: string,
options?: UseMutationOptions<InstallTranslatedMigrationRulesResponse, Error>
) => {
const invalidateGetRuleMigrationsQuery = useInvalidateGetMigrationRulesQuery(migrationId);
const invalidateGetMigrationTranslationStatsQuery =
useInvalidateGetMigrationTranslationStatsQuery(migrationId);
return useMutation<InstallTranslatedMigrationRulesResponse, Error>(
() => installTranslatedMigrationRules({ migrationId }),
@ -29,6 +32,7 @@ export const useInstallAllMigrationRulesMutation = (
mutationKey: INSTALL_ALL_MIGRATION_RULES_MUTATION_KEY,
onSettled: (...args) => {
invalidateGetRuleMigrationsQuery();
invalidateGetMigrationTranslationStatsQuery();
if (options?.onSettled) {
options.onSettled(...args);

View file

@ -28,9 +28,9 @@ export const BulkActions: React.FC<BulkActionsProps> = React.memo(
installTranslatedRule,
installSelectedRule,
}) => {
const showInstallTranslatedRulesButton = numberOfTranslatedRules > 0;
const disableInstallTranslatedRulesButton = isTableLoading || !numberOfTranslatedRules;
const showInstallSelectedRulesButton =
showInstallTranslatedRulesButton && numberOfSelectedRules > 0;
disableInstallTranslatedRulesButton && numberOfSelectedRules > 0;
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{showInstallSelectedRulesButton ? (
@ -46,21 +46,21 @@ export const BulkActions: React.FC<BulkActionsProps> = React.memo(
</EuiButton>
</EuiFlexItem>
) : null}
{showInstallTranslatedRulesButton ? (
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="plusInCircle"
data-test-subj="installTranslatedRulesButton"
onClick={installTranslatedRule}
disabled={isTableLoading}
aria-label={i18n.INSTALL_ALL_ARIA_LABEL}
>
{i18n.INSTALL_ALL_RULES(numberOfTranslatedRules)}
{isTableLoading && <EuiLoadingSpinner size="s" />}
</EuiButton>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="plusInCircle"
data-test-subj="installTranslatedRulesButton"
onClick={installTranslatedRule}
disabled={disableInstallTranslatedRulesButton}
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>
);
}

View file

@ -1,54 +0,0 @@
/*
* 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 { EuiFlexGroup } from '@elastic/eui';
import type { Dispatch, SetStateAction } from 'react';
import React, { useCallback } from 'react';
import * as i18n from './translations';
import { RuleSearchField } from '../../../../detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_search_field';
import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install';
export interface FiltersComponentProps {
/**
* Currently selected table filter
*/
filterOptions: TableFilterOptions;
/**
* Handles filter options changes
*/
setFilterOptions: Dispatch<SetStateAction<TableFilterOptions>>;
}
/**
* Collection of filters for filtering data within the SIEM Rules Migrations table.
* Contains search bar and tag selection
*/
const FiltersComponent: React.FC<FiltersComponentProps> = ({ filterOptions, setFilterOptions }) => {
const handleOnSearch = useCallback(
(filterString: string) => {
setFilterOptions((filters) => ({
...filters,
filter: filterString.trim(),
}));
},
[setFilterOptions]
);
return (
<EuiFlexGroup gutterSize="m" justifyContent="flexEnd" wrap>
<RuleSearchField
initialValue={filterOptions.filter}
onSearch={handleOnSearch}
placeholder={i18n.SEARCH_PLACEHOLDER}
/>
</EuiFlexGroup>
);
};
export const Filters = React.memo(FiltersComponent);
Filters.displayName = 'Filters';

View file

@ -5,33 +5,30 @@
* 2.0.
*/
import type { CriteriaWithPagination } from '@elastic/eui';
import {
EuiInMemoryTable,
EuiSkeletonLoading,
EuiProgress,
EuiSkeletonTitle,
EuiSkeletonText,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiBasicTable,
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import {
RULES_TABLE_INITIAL_PAGE_SIZE,
RULES_TABLE_PAGE_SIZE_OPTIONS,
} from '../../../../detection_engine/rule_management_ui/components/rules_table/constants';
import { NoItemsMessage } from './no_items_message';
import { Filters } from './filters';
import { useRulesTableColumns } from '../../hooks/use_rules_table_columns';
import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install';
import { useFilterRulesToInstall } from '../../hooks/use_filter_rules_to_install';
import { useRulePreviewFlyout } from '../../hooks/use_rule_preview_flyout';
import { useInstallMigrationRules } from '../../logic/use_install_migration_rules';
import { useGetMigrationRules } from '../../logic/use_get_migration_rules';
import { useInstallAllMigrationRules } from '../../logic/use_install_all_migration_rules';
import { useInstallTranslatedMigrationRules } from '../../logic/use_install_translated_migration_rules';
import { BulkActions } from './bulk_actions';
import { useGetMigrationTranslationStats } from '../../logic/use_get_migration_translation_stats';
import { SearchField } from './search_field';
const DEFAULT_PAGE_SIZE = 10;
export interface RulesTableComponentProps {
/**
@ -44,29 +41,47 @@ export interface RulesTableComponentProps {
* Table Component for displaying SIEM rules migrations
*/
const RulesTableComponent: React.FC<RulesTableComponentProps> = ({ migrationId }) => {
const { data: ruleMigrations, isLoading: isDataLoading } = useGetMigrationRules(migrationId);
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [searchTerm, setSearchTerm] = useState<string | undefined>();
const { data: translationStats, isLoading: isStatsLoading } =
useGetMigrationTranslationStats(migrationId);
const {
data: { ruleMigrations, total } = { ruleMigrations: [], total: 0 },
isLoading: isDataLoading,
} = useGetMigrationRules({
migrationId,
page: pageIndex,
perPage: pageSize,
searchTerm,
});
const [selectedRuleMigrations, setSelectedRuleMigrations] = useState<RuleMigration[]>([]);
const [filterOptions, setFilterOptions] = useState<TableFilterOptions>({
filter: '',
});
const pagination = useMemo(() => {
return {
pageIndex,
pageSize,
totalItemCount: total,
};
}, [pageIndex, pageSize, total]);
const filteredRuleMigrations = useFilterRulesToInstall({
filterOptions,
ruleMigrations: ruleMigrations ?? [],
});
const onTableChange = useCallback(({ page, sort }: CriteriaWithPagination<RuleMigration>) => {
if (page) {
setPageIndex(page.index);
setPageSize(page.size);
}
}, []);
const handleOnSearch = useCallback((value: string) => {
setSearchTerm(value.trim());
}, []);
const { mutateAsync: installMigrationRules } = useInstallMigrationRules(migrationId);
const { mutateAsync: installAllMigrationRules } = useInstallAllMigrationRules(migrationId);
const numberOfTranslatedRules = useMemo(() => {
return filteredRuleMigrations.filter(
(rule) =>
!rule.elastic_rule?.id &&
(rule.elastic_rule?.prebuilt_rule_id || rule.translation_result === 'full')
).length;
}, [filteredRuleMigrations]);
const { mutateAsync: installTranslatedMigrationRules } =
useInstallTranslatedMigrationRules(migrationId);
const [isTableLoading, setTableLoading] = useState(false);
const installSingleRule = useCallback(
@ -85,12 +100,12 @@ const RulesTableComponent: React.FC<RulesTableComponentProps> = ({ migrationId }
async (enable?: boolean) => {
setTableLoading(true);
try {
await installAllMigrationRules();
await installTranslatedMigrationRules();
} finally {
setTableLoading(false);
}
},
[installAllMigrationRules]
[installTranslatedMigrationRules]
);
const ruleActionsFactory = useCallback(
@ -105,8 +120,6 @@ const RulesTableComponent: React.FC<RulesTableComponentProps> = ({ migrationId }
ruleActionsFactory,
});
const shouldShowProgress = isDataLoading;
const rulesColumns = useRulesTableColumns({
disableActions: isTableLoading,
openMigrationRulePreview: openRulePreview,
@ -115,14 +128,6 @@ const RulesTableComponent: React.FC<RulesTableComponentProps> = ({ migrationId }
return (
<>
{shouldShowProgress && (
<EuiProgress
data-test-subj="loadingRulesInfoProgress"
size="xs"
position="absolute"
color="accent"
/>
)}
<EuiSkeletonLoading
isLoading={isDataLoading}
loadingContent={
@ -132,39 +137,36 @@ const RulesTableComponent: React.FC<RulesTableComponentProps> = ({ migrationId }
</>
}
loadedContent={
!filteredRuleMigrations.length ? (
!translationStats?.rules.total ? (
<NoItemsMessage />
) : (
<>
<EuiFlexGroup gutterSize="m" justifyContent="flexEnd" wrap>
<EuiFlexItem>
<Filters filterOptions={filterOptions} setFilterOptions={setFilterOptions} />
<SearchField initialValue={searchTerm} onSearch={handleOnSearch} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<BulkActions
isTableLoading={isDataLoading || isTableLoading}
numberOfTranslatedRules={numberOfTranslatedRules}
isTableLoading={isStatsLoading || isDataLoading || isTableLoading}
numberOfTranslatedRules={translationStats?.rules.installable ?? 0}
numberOfSelectedRules={0}
installTranslatedRule={installTranslatedRules}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiInMemoryTable
<EuiBasicTable<RuleMigration>
loading={isTableLoading}
items={filteredRuleMigrations}
sorting
pagination={{
initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE,
pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS,
}}
items={ruleMigrations}
pagination={pagination}
onChange={onTableChange}
selection={{
selectable: () => true,
onSelectionChange: setSelectedRuleMigrations,
initialSelected: selectedRuleMigrations,
}}
itemId="rule_id"
data-test-subj="rules-translation-table"
itemId={'id'}
data-test-subj={'rules-translation-table'}
columns={rulesColumns}
/>
</>

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 type { ChangeEvent } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { EuiFieldSearch, EuiFlexItem } from '@elastic/eui';
import * as i18n from './translations';
const SearchBarWrapper = styled(EuiFlexItem)`
min-width: 200px;
& .euiPopover {
// This is needed to "cancel" styles passed down from EuiTourStep that
// interfere with EuiFieldSearch and don't allow it to take the full width
display: block;
}
`;
interface SearchFieldProps {
initialValue?: string;
onSearch: (value: string) => void;
placeholder?: string;
}
export const SearchField: React.FC<SearchFieldProps> = React.memo(
({ initialValue, onSearch, placeholder }) => {
const [searchText, setSearchText] = useState(initialValue);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setSearchText(e.target.value),
[setSearchText]
);
useEffect(() => {
setSearchText(initialValue);
}, [initialValue]);
return (
<SearchBarWrapper grow>
<EuiFieldSearch
aria-label={i18n.SEARCH_MIGRATION_RULES}
fullWidth
incremental={false}
placeholder={placeholder ?? i18n.SEARCH_MIGRATION_RULES_PLACEHOLDER}
value={searchText}
onChange={handleChange}
onSearch={onSearch}
data-test-subj="ruleSearchField"
/>
</SearchBarWrapper>
);
}
);
SearchField.displayName = 'SearchField';

View file

@ -7,10 +7,17 @@
import { i18n } from '@kbn/i18n';
export const SEARCH_PLACEHOLDER = i18n.translate(
export const SEARCH_MIGRATION_RULES = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.searchAriaLabel',
{
defaultMessage: 'Search migration rules',
}
);
export const SEARCH_MIGRATION_RULES_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.searchBarPlaceholder',
{
defaultMessage: 'Search by rule name',
defaultMessage: 'Search by migration rule name',
}
);
@ -42,12 +49,22 @@ export const INSTALL_SELECTED_RULES = (numberOfSelectedRules: number) => {
});
};
export const INSTALL_ALL_RULES = (numberOfAllRules: number) => {
return i18n.translate('xpack.securitySolution.siemMigrations.rules.table.installAllRules', {
defaultMessage:
'Install translated {numberOfAllRules, plural, one {rule} other {rules}} ({numberOfAllRules})',
values: { numberOfAllRules },
});
export const INSTALL_TRANSLATED_RULES_EMPTY_STATE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.installTranslatedRulesEmptyState',
{
defaultMessage: 'Install translated rules',
}
);
export const INSTALL_TRANSLATED_RULES = (numberOfAllRules: number) => {
return i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.installTranslatedRules',
{
defaultMessage:
'Install translated {numberOfAllRules, plural, one {rule} other {rules}} ({numberOfAllRules})',
values: { numberOfAllRules },
}
);
};
export const INSTALL_SELECTED_ARIA_LABEL = i18n.translate(
@ -57,8 +74,8 @@ export const INSTALL_SELECTED_ARIA_LABEL = i18n.translate(
}
);
export const INSTALL_ALL_ARIA_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.installAllButtonAriaLabel',
export const INSTALL_TRANSLATED_ARIA_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.installTranslatedButtonAriaLabel',
{
defaultMessage: 'Install all translated rules',
}

View file

@ -12,3 +12,4 @@ export * from './name';
export * from './risk_score';
export * from './severity';
export * from './status';
export * from './updated';

View file

@ -69,3 +69,10 @@ export const COLUMN_SEVERITY = i18n.translate(
defaultMessage: 'Severity',
}
);
export const COLUMN_UPDATED = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.updatedLabel',
{
defaultMessage: 'Updated',
}
);

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
export const createUpdatedColumn = (): TableColumn => {
return {
field: 'updated_at',
name: i18n.COLUMN_UPDATED,
render: (value: RuleMigration['updated_at']) => (
<FormattedRelativePreferenceDate value={value} dateFormat="M/D/YY" />
),
sortable: true,
truncateText: false,
align: 'center',
width: '10%',
};
};

View file

@ -30,7 +30,7 @@ const StatusBadgeComponent: React.FC<Props> = ({
installedRuleId,
'data-test-subj': dataTestSubj = 'translation-result',
}) => {
const translationResult = installedRuleId || !value ? 'full' : value;
const translationResult = installedRuleId ? 'full' : value ?? 'untranslatable';
const displayValue = convertTranslationResultIntoText(translationResult);
const color = statusToColorMap[translationResult];

View file

@ -24,7 +24,10 @@ import type { RuleMigration } from '../../../../../../common/siem_migrations/mod
import { TranslationTabHeader } from './header';
import { RuleQueryComponent } from './rule_query';
import * as i18n from './translations';
import { convertTranslationResultIntoText } from '../../../utils/helpers';
import {
convertTranslationResultIntoColor,
convertTranslationResultIntoText,
} from '../../../utils/helpers';
interface TranslationTabProps {
ruleMigration: RuleMigration;
@ -66,9 +69,7 @@ export const TranslationTab = ({ ruleMigration }: TranslationTabProps) => {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge
iconSide="right"
iconType="arrowDown"
color="primary"
color={convertTranslationResultIntoColor(ruleMigration.translation_result)}
onClick={() => {}}
onClickAriaLabel={'Click to update translation status'}
>

View file

@ -1,33 +0,0 @@
/*
* 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 { useMemo } from 'react';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { FilterOptions } from '../../../detection_engine/rule_management/logic/types';
export type TableFilterOptions = Pick<FilterOptions, 'filter'>;
export const useFilterRulesToInstall = ({
ruleMigrations,
filterOptions,
}: {
ruleMigrations: RuleMigration[];
filterOptions: TableFilterOptions;
}) => {
const filteredRules = useMemo(() => {
const { filter } = filterOptions;
return ruleMigrations.filter((migration) => {
const name = migration.elastic_rule?.title ?? migration.original_rule.title;
if (filter && !name.toLowerCase().includes(filter.toLowerCase())) {
return false;
}
return true;
});
}, [filterOptions, ruleMigrations]);
return filteredRules;
};

View file

@ -14,6 +14,7 @@ import {
createRiskScoreColumn,
createSeverityColumn,
createStatusColumn,
createUpdatedColumn,
} from '../components/rules_table_columns';
export const useRulesTableColumns = ({
@ -27,6 +28,7 @@ export const useRulesTableColumns = ({
}): TableColumn[] => {
return useMemo(
() => [
createUpdatedColumn(),
createNameColumn({ openMigrationRulePreview }),
createStatusColumn(),
createRiskScoreColumn(),

View file

@ -14,6 +14,13 @@ export const GET_MIGRATION_RULES_FAILURE = i18n.translate(
}
);
export const GET_MIGRATION_TRANSLATION_STATS_FAILURE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.getMigrationTranslationStatsFailDescription',
{
defaultMessage: 'Failed to fetch migration translation stats',
}
);
export const INSTALL_MIGRATION_RULES_FAILURE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.installMigrationRulesFailDescription',
{

View file

@ -9,10 +9,15 @@ import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { useGetMigrationRulesQuery } from '../api/hooks/use_get_migration_rules_query';
import * as i18n from './translations';
export const useGetMigrationRules = (migrationId: string) => {
export const useGetMigrationRules = (params: {
migrationId: string;
page?: number;
perPage?: number;
searchTerm?: string;
}) => {
const { addError } = useAppToasts();
return useGetMigrationRulesQuery(migrationId, {
return useGetMigrationRulesQuery(params, {
onError: (error) => {
addError(error, { title: i18n.GET_MIGRATION_RULES_FAILURE });
},

View file

@ -0,0 +1,20 @@
/*
* 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 { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { useGetMigrationTranslationStatsQuery } from '../api/hooks/use_get_migration_translation_stats_query';
import * as i18n from './translations';
export const useGetMigrationTranslationStats = (migrationId: string) => {
const { addError } = useAppToasts();
return useGetMigrationTranslationStatsQuery(migrationId, {
onError: (error) => {
addError(error, { title: i18n.GET_MIGRATION_TRANSLATION_STATS_FAILURE });
},
});
};

View file

@ -6,13 +6,13 @@
*/
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { useInstallAllMigrationRulesMutation } from '../api/hooks/use_install_all_migration_rules_mutation';
import { useInstallTranslatedMigrationRulesMutation } from '../api/hooks/use_install_translated_migration_rules_mutation';
import * as i18n from './translations';
export const useInstallAllMigrationRules = (migrationId: string) => {
export const useInstallTranslatedMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
return useInstallAllMigrationRulesMutation(migrationId, {
return useInstallTranslatedMigrationRulesMutation(migrationId, {
onError: (error) => {
addError(error, { title: i18n.INSTALL_MIGRATION_RULES_FAILURE });
},

View file

@ -11,6 +11,22 @@ import {
} from '../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => {
switch (status) {
case RuleMigrationTranslationResultEnum.full:
return 'primary';
case RuleMigrationTranslationResultEnum.partial:
return 'warning';
case RuleMigrationTranslationResultEnum.untranslatable:
return 'danger';
default:
throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status));
}
};
export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => {
switch (status) {
case RuleMigrationTranslationResultEnum.full:
@ -23,6 +39,6 @@ export const convertTranslationResultIntoText = (status?: RuleMigrationTranslati
return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL;
default:
return i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL;
throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status));
}
};

View file

@ -34,3 +34,13 @@ export const SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL = i18n.translate(
defaultMessage: 'Unknown',
}
);
export const SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR = (status?: string) => {
return i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationResult.unknownError',
{
defaultMessage: 'Unknown translation result status: ({status})',
values: { status },
}
);
};

View file

@ -9,6 +9,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import {
GetRuleMigrationRequestParams,
GetRuleMigrationRequestQuery,
type GetRuleMigrationResponse,
} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants';
@ -29,18 +30,35 @@ export const registerSiemRuleMigrationsGetRoute = (
{
version: '1',
validate: {
request: { params: buildRouteValidationWithZod(GetRuleMigrationRequestParams) },
request: {
params: buildRouteValidationWithZod(GetRuleMigrationRequestParams),
query: buildRouteValidationWithZod(GetRuleMigrationRequestQuery),
},
},
},
withLicense(async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationResponse>> => {
const migrationId = req.params.migration_id;
const { migration_id: migrationId } = req.params;
const { page, per_page: perPage, search_term: searchTerm } = req.query;
try {
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const migrationRules = await ruleMigrationsClient.data.rules.get({ migrationId });
let from = 0;
if (page && perPage) {
from = page * perPage;
}
const size = perPage;
return res.ok({ body: migrationRules });
const result = await ruleMigrationsClient.data.rules.get(
{
migrationId,
searchTerm,
},
from,
size
);
return res.ok({ body: result });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });

View file

@ -12,13 +12,14 @@ import { registerSiemRuleMigrationsUpdateRoute } from './update';
import { registerSiemRuleMigrationsGetRoute } from './get';
import { registerSiemRuleMigrationsStartRoute } from './start';
import { registerSiemRuleMigrationsStatsRoute } from './stats';
import { registerSiemRuleMigrationsTranslationStatsRoute } from './translation_stats';
import { registerSiemRuleMigrationsStopRoute } from './stop';
import { registerSiemRuleMigrationsStatsAllRoute } from './stats_all';
import { registerSiemRuleMigrationsResourceUpsertRoute } from './resources/upsert';
import { registerSiemRuleMigrationsResourceGetRoute } from './resources/get';
import { registerSiemRuleMigrationsRetryRoute } from './retry';
import { registerSiemRuleMigrationsInstallRoute } from './rules/install';
import { registerSiemRuleMigrationsInstallTranslatedRoute } from './rules/install_translated';
import { registerSiemRuleMigrationsInstallRoute } from './install';
import { registerSiemRuleMigrationsInstallTranslatedRoute } from './install_translated';
export const registerSiemRuleMigrationsRoutes = (
router: SecuritySolutionPluginRouter,
@ -31,6 +32,7 @@ export const registerSiemRuleMigrationsRoutes = (
registerSiemRuleMigrationsStartRoute(router, logger);
registerSiemRuleMigrationsRetryRoute(router, logger);
registerSiemRuleMigrationsStatsRoute(router, logger);
registerSiemRuleMigrationsTranslationStatsRoute(router, logger);
registerSiemRuleMigrationsStopRoute(router, logger);
registerSiemRuleMigrationsInstallRoute(router, logger);
registerSiemRuleMigrationsInstallTranslatedRoute(router, logger);

View file

@ -7,15 +7,15 @@
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { SIEM_RULE_MIGRATION_INSTALL_PATH } from '../../../../../../common/siem_migrations/constants';
import type { InstallMigrationRulesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_INSTALL_PATH } from '../../../../../common/siem_migrations/constants';
import type { InstallMigrationRulesResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import {
InstallMigrationRulesRequestBody,
InstallMigrationRulesRequestParams,
} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { withLicense } from '../util/with_license';
import { installTranslated } from '../util/installation';
} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { withLicense } from './util/with_license';
import { installTranslated } from './util/installation';
export const registerSiemRuleMigrationsInstallRoute = (
router: SecuritySolutionPluginRouter,

View file

@ -7,12 +7,12 @@
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH } from '../../../../../../common/siem_migrations/constants';
import type { InstallTranslatedMigrationRulesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { InstallTranslatedMigrationRulesRequestParams } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { withLicense } from '../util/with_license';
import { installTranslated } from '../util/installation';
import { SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH } from '../../../../../common/siem_migrations/constants';
import type { InstallTranslatedMigrationRulesResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { InstallTranslatedMigrationRulesRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { withLicense } from './util/with_license';
import { installTranslated } from './util/installation';
export const registerSiemRuleMigrationsInstallTranslatedRoute = (
router: SecuritySolutionPluginRouter,

View file

@ -0,0 +1,56 @@
/*
* 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 type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import type { GetRuleMigrationTranslationStatsResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationTranslationStatsRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH } from '../../../../../common/siem_migrations/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { withLicense } from './util/with_license';
export const registerSiemRuleMigrationsTranslationStatsRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.get({
path: SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
access: 'internal',
security: { authz: { requiredPrivileges: ['securitySolution'] } },
})
.addVersion(
{
version: '1',
validate: {
request: {
params: buildRouteValidationWithZod(GetRuleMigrationTranslationStatsRequestParams),
},
},
},
withLicense(
async (
context,
req,
res
): Promise<IKibanaResponse<GetRuleMigrationTranslationStatsResponse>> => {
const migrationId = req.params.migration_id;
try {
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const stats = await ruleMigrationsClient.data.rules.getTranslationStats(migrationId);
return res.ok({ body: stats });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });
}
}
)
);
};

View file

@ -177,7 +177,7 @@ export const installTranslated = async ({
const detectionRulesClient = securitySolutionContext.getDetectionRulesClient();
const ruleMigrationsClient = securitySolutionContext.getSiemRuleMigrationsClient();
const rulesToInstall = await ruleMigrationsClient.data.rules.get({
const { data: rulesToInstall } = await ruleMigrationsClient.data.rules.get({
migrationId,
ids,
installable: true,

View file

@ -15,11 +15,15 @@ import type {
QueryDslQueryContainer,
} from '@elastic/elasticsearch/lib/api/types';
import type { StoredRuleMigration } from '../types';
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import {
SiemMigrationRuleTranslationResult,
SiemMigrationStatus,
} from '../../../../../common/siem_migrations/constants';
import type {
ElasticRule,
RuleMigration,
RuleMigrationTaskStats,
RuleMigrationTranslationStats,
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client';
@ -39,6 +43,7 @@ export interface RuleMigrationFilterOptions {
status?: SiemMigrationStatus | SiemMigrationStatus[];
ids?: string[];
installable?: boolean;
searchTerm?: string;
}
/* BULK_MAX_SIZE defines the number to break down the bulk operations by.
@ -46,6 +51,20 @@ export interface RuleMigrationFilterOptions {
*/
const BULK_MAX_SIZE = 500 as const;
const getInstallableConditions = (): QueryDslQueryContainer[] => {
return [
{ term: { translation_result: SiemMigrationRuleTranslationResult.FULL } },
{
nested: {
path: 'elastic_rule',
query: {
bool: { must_not: { exists: { field: 'elastic_rule.id' } } },
},
},
},
];
};
export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient {
/** Indexes an array of rule migrations to be processed */
async create(ruleMigrations: CreateRuleMigrationInput[]): Promise<void> {
@ -108,18 +127,24 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
}
/** Retrieves an array of rule documents of a specific migrations */
async get(filters: RuleMigrationFilterOptions): Promise<StoredRuleMigration[]> {
async get(
filters: RuleMigrationFilterOptions,
from?: number,
size?: number
): Promise<{ total: number; data: StoredRuleMigration[] }> {
const index = await this.getIndexName();
const query = this.getFilterQuery(filters);
const storedRuleMigrations = await this.esClient
.search<RuleMigration>({ index, query, sort: '_doc' })
.then(this.processResponseHits.bind(this))
const result = await this.esClient
.search<RuleMigration>({ index, query, sort: '_doc', from, size })
.catch((error) => {
this.logger.error(`Error searching rule migrations: ${error.message}`);
throw error;
});
return storedRuleMigrations;
return {
total: this.getTotalHits(result),
data: this.processResponseHits(result),
};
}
/**
@ -217,6 +242,49 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
});
}
/** Retrieves the translation stats for the rule migrations with the provided id */
async getTranslationStats(migrationId: string): Promise<RuleMigrationTranslationStats> {
const index = await this.getIndexName();
const query = this.getFilterQuery({ migrationId });
const aggregations = {
prebuilt: {
filter: {
nested: {
path: 'elastic_rule',
query: { exists: { field: 'elastic_rule.prebuilt_rule_id' } },
},
},
},
installable: {
filter: {
bool: {
must: getInstallableConditions(),
},
},
},
};
const result = await this.esClient
.search({ index, query, aggregations, _source: false })
.catch((error) => {
this.logger.error(`Error getting rule migrations stats: ${error.message}`);
throw error;
});
const bucket = result.aggregations ?? {};
const total = this.getTotalHits(result);
const prebuilt = (bucket.prebuilt as AggregationsFilterAggregate)?.doc_count ?? 0;
return {
id: migrationId,
rules: {
total,
prebuilt,
custom: total - prebuilt,
installable: (bucket.installable as AggregationsFilterAggregate)?.doc_count ?? 0,
},
};
}
/** Retrieves the stats for the rule migrations with the provided id */
async getStats(migrationId: string): Promise<RuleMigrationDataStats> {
const index = await this.getIndexName();
@ -295,6 +363,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
status,
ids,
installable,
searchTerm,
}: RuleMigrationFilterOptions): QueryDslQueryContainer {
const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }];
if (status) {
@ -308,15 +377,15 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
filter.push({ terms: { _id: ids } });
}
if (installable) {
filter.push(
{ term: { translation_result: 'full' } },
{
nested: {
path: 'elastic_rule',
query: { bool: { must_not: { exists: { field: 'elastic_rule.id' } } } },
},
}
);
filter.push(...getInstallableConditions());
}
if (searchTerm?.length) {
filter.push({
nested: {
path: 'elastic_rule',
query: { match: { 'elastic_rule.title': searchTerm } },
},
});
}
return { bool: { filter } };
}

View file

@ -19,17 +19,17 @@ export const ruleMigrationsFieldMap: FieldMap<SchemaFieldMapKeys<Omit<RuleMigrat
original_rule: { type: 'nested', required: true },
'original_rule.vendor': { type: 'keyword', required: true },
'original_rule.id': { type: 'keyword', required: true },
'original_rule.title': { type: 'keyword', required: true },
'original_rule.description': { type: 'keyword', required: false },
'original_rule.title': { type: 'text', required: true },
'original_rule.description': { type: 'text', required: false },
'original_rule.query': { type: 'text', required: true },
'original_rule.query_language': { type: 'keyword', required: true },
'original_rule.mitre_attack_ids': { type: 'keyword', array: true, required: false },
elastic_rule: { type: 'nested', required: false },
'elastic_rule.title': { type: 'keyword', required: true },
'elastic_rule.title': { type: 'text', required: true },
'elastic_rule.integration_ids': { type: 'keyword', array: true, required: false },
'elastic_rule.query': { type: 'text', required: true },
'elastic_rule.query_language': { type: 'keyword', required: true },
'elastic_rule.description': { type: 'keyword', required: false },
'elastic_rule.description': { type: 'text', required: false },
'elastic_rule.severity': { type: 'keyword', required: false },
'elastic_rule.prebuilt_rule_id': { type: 'keyword', required: false },
'elastic_rule.id': { type: 'keyword', required: false },

View file

@ -92,12 +92,16 @@ import {
GetRuleExecutionResultsRequestQueryInput,
GetRuleExecutionResultsRequestParamsInput,
} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen';
import { GetRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
GetRuleMigrationResourcesRequestQueryInput,
GetRuleMigrationResourcesRequestParamsInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationTranslationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timeline/get_timeline_route.gen';
import { GetTimelinesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timelines/get_timelines_route.gen';
import { ImportRulesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/import_rules/import_rules_route.gen';
@ -937,7 +941,8 @@ finalize it.
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query);
},
/**
* Retrieves resources for an existing SIEM rules migration
@ -973,6 +978,27 @@ finalize it.
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Retrieves the translation stats of a SIEM rules migration using the migration id provided
*/
getRuleMigrationTranslationStats(
props: GetRuleMigrationTranslationStatsProps,
kibanaSpace: string = 'default'
) {
return supertest
.get(
routeWithNamespace(
replaceParams(
'/internal/siem_migrations/rules/{migration_id}/translation_stats',
props.params
),
kibanaSpace
)
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Get the details of an existing saved Timeline or Timeline template.
*/
@ -1667,6 +1693,7 @@ export interface GetRuleExecutionResultsProps {
params: GetRuleExecutionResultsRequestParamsInput;
}
export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput;
}
export interface GetRuleMigrationResourcesProps {
@ -1676,6 +1703,9 @@ export interface GetRuleMigrationResourcesProps {
export interface GetRuleMigrationStatsProps {
params: GetRuleMigrationStatsRequestParamsInput;
}
export interface GetRuleMigrationTranslationStatsProps {
params: GetRuleMigrationTranslationStatsRequestParamsInput;
}
export interface GetTimelineProps {
query: GetTimelineRequestQueryInput;
}