[Rules migration] Add install and install all migration rules endpoints (#11283) (#202026)

## Summary

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

With these changes we two new routes:

* `/internal/siem_migrations/rules/install`: allows to install a
specific set of migration rules
* `/internal/siem_migrations/rules/install_translated`: allows to
install all translated rules in specified migration

Also we connect these two new API calls with the "Install" button within
the "migration rules" table and the "Install translated rules" button on
the "SIEM migration rules" page.

### Screenshots


https://github.com/user-attachments/assets/29390d07-eab5-4157-8958-1e3f8459db09

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Sergi Massaneda <sergi.massaneda@gmail.com>
This commit is contained in:
Ievgen Sorokopud 2024-11-29 18:05:20 +01:00 committed by GitHub
parent 8d2e28aef2
commit 07fbb92585
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1361 additions and 161 deletions

View file

@ -359,6 +359,11 @@ import type {
GetRuleMigrationResourcesResponse,
GetRuleMigrationStatsRequestParamsInput,
GetRuleMigrationStatsResponse,
InstallMigrationRulesRequestParamsInput,
InstallMigrationRulesRequestBodyInput,
InstallMigrationRulesResponse,
InstallTranslatedMigrationRulesRequestParamsInput,
InstallTranslatedMigrationRulesResponse,
StartRuleMigrationRequestParamsInput,
StartRuleMigrationRequestBodyInput,
StartRuleMigrationResponse,
@ -1559,6 +1564,22 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Installs migration rules
*/
async installMigrationRules(props: InstallMigrationRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API InstallMigrationRules`);
return this.kbnClient
.request<InstallMigrationRulesResponse>({
path: replaceParams('/internal/siem_migrations/rules/{migration_id}/install', props.params),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'POST',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Install and update all Elastic prebuilt detection rules and Timelines.
*/
@ -1590,6 +1611,24 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Installs all translated migration rules
*/
async installTranslatedMigrationRules(props: InstallTranslatedMigrationRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API InstallTranslatedMigrationRules`);
return this.kbnClient
.request<InstallTranslatedMigrationRulesResponse>({
path: replaceParams(
'/internal/siem_migrations/rules/{migration_id}/install_translated',
props.params
),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'POST',
})
.catch(catchAxiosErrorFormatAndThrow);
}
async internalUploadAssetCriticalityRecords(props: InternalUploadAssetCriticalityRecordsProps) {
this.log.info(`${new Date().toISOString()} Calling API InternalUploadAssetCriticalityRecords`);
return this.kbnClient
@ -2324,9 +2363,16 @@ export interface InitEntityEngineProps {
export interface InitEntityStoreProps {
body: InitEntityStoreRequestBodyInput;
}
export interface InstallMigrationRulesProps {
params: InstallMigrationRulesRequestParamsInput;
body: InstallMigrationRulesRequestBodyInput;
}
export interface InstallPrepackedTimelinesProps {
body: InstallPrepackedTimelinesRequestBodyInput;
}
export interface InstallTranslatedMigrationRulesProps {
params: InstallTranslatedMigrationRulesRequestParamsInput;
}
export interface InternalUploadAssetCriticalityRecordsProps {
attachment: FormData;
}

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
export const SIEM_MIGRATIONS_PATH = '/internal/siem_migrations' as const;
export const SIEM_RULE_MIGRATIONS_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const;
@ -14,6 +16,9 @@ export const SIEM_RULE_MIGRATION_START_PATH = `${SIEM_RULE_MIGRATION_PATH}/start
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_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 =
`${SIEM_RULE_MIGRATION_PATH}/install_translated` as const;
export const SIEM_RULE_MIGRATION_RESOURCES_PATH = `${SIEM_RULE_MIGRATION_PATH}/resources` as const;
@ -36,3 +41,6 @@ export enum SiemMigrationRuleTranslationResult {
PARTIAL = 'partial',
UNTRANSLATABLE = 'untranslatable',
}
export const DEFAULT_TRANSLATION_RISK_SCORE = 21;
export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low';

View file

@ -88,6 +88,48 @@ export type GetRuleMigrationStatsRequestParamsInput = z.input<
export type GetRuleMigrationStatsResponse = z.infer<typeof GetRuleMigrationStatsResponse>;
export const GetRuleMigrationStatsResponse = RuleMigrationTaskStats;
export type InstallMigrationRulesRequestParams = z.infer<typeof InstallMigrationRulesRequestParams>;
export const InstallMigrationRulesRequestParams = z.object({
migration_id: NonEmptyString,
});
export type InstallMigrationRulesRequestParamsInput = z.input<
typeof InstallMigrationRulesRequestParams
>;
export type InstallMigrationRulesRequestBody = z.infer<typeof InstallMigrationRulesRequestBody>;
export const InstallMigrationRulesRequestBody = z.array(NonEmptyString);
export type InstallMigrationRulesRequestBodyInput = z.input<
typeof InstallMigrationRulesRequestBody
>;
export type InstallMigrationRulesResponse = z.infer<typeof InstallMigrationRulesResponse>;
export const InstallMigrationRulesResponse = z.object({
/**
* Indicates rules migrations have been installed.
*/
installed: z.boolean(),
});
export type InstallTranslatedMigrationRulesRequestParams = z.infer<
typeof InstallTranslatedMigrationRulesRequestParams
>;
export const InstallTranslatedMigrationRulesRequestParams = z.object({
migration_id: NonEmptyString,
});
export type InstallTranslatedMigrationRulesRequestParamsInput = z.input<
typeof InstallTranslatedMigrationRulesRequestParams
>;
export type InstallTranslatedMigrationRulesResponse = z.infer<
typeof InstallTranslatedMigrationRulesResponse
>;
export const InstallTranslatedMigrationRulesResponse = z.object({
/**
* Indicates rules migrations have been installed.
*/
installed: z.boolean(),
});
export type StartRuleMigrationRequestParams = z.infer<typeof StartRuleMigrationRequestParams>;
export const StartRuleMigrationRequestParams = z.object({
migration_id: NonEmptyString,

View file

@ -81,6 +81,73 @@ paths:
type: boolean
description: Indicates rules migrations have been updated.
/internal/siem_migrations/rules/{migration_id}/install:
post:
summary: Installs translated migration rules
operationId: InstallMigrationRules
x-codegen-enabled: true
description: Installs migration rules
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to isnstall rules for
$ref: '../../common.schema.yaml#/components/schemas/NonEmptyString'
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
description: The rule migration id
$ref: '../../common.schema.yaml#/components/schemas/NonEmptyString'
responses:
200:
description: Indicates rules migrations have been installed correctly.
content:
application/json:
schema:
type: object
required:
- installed
properties:
installed:
type: boolean
description: Indicates rules migrations have been installed.
/internal/siem_migrations/rules/{migration_id}/install_translated:
post:
summary: Installs all translated migration rules
operationId: InstallTranslatedMigrationRules
x-codegen-enabled: true
description: Installs all translated migration rules
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to install translated rules for
$ref: '../../common.schema.yaml#/components/schemas/NonEmptyString'
responses:
200:
description: Indicates rules migrations have been installed correctly.
content:
application/json:
schema:
type: object
required:
- installed
properties:
installed:
type: boolean
description: Indicates rules migrations have been installed.
/internal/siem_migrations/rules/stats:
get:
summary: Retrieves the stats for all rule migrations

View file

@ -11,14 +11,19 @@ import { KibanaServices } from '../../../common/lib/kibana';
import {
SIEM_RULE_MIGRATIONS_ALL_STATS_PATH,
SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH,
SIEM_RULE_MIGRATION_INSTALL_PATH,
SIEM_RULE_MIGRATION_PATH,
SIEM_RULE_MIGRATION_START_PATH,
} from '../../../../common/siem_migrations/constants';
import type {
GetAllStatsRuleMigrationResponse,
GetRuleMigrationResponse,
InstallTranslatedMigrationRulesResponse,
InstallMigrationRulesResponse,
StartRuleMigrationRequestBody,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { InstallTranslatedRulesProps, InstallRulesProps } from '../types';
/**
* Retrieves the stats for all the existing migrations, aggregated by `migration_id`.
@ -82,3 +87,33 @@ export const getRuleMigrations = async ({
{ method: 'GET', version: '1', signal }
);
};
export const installMigrationRules = async ({
migrationId,
ids,
signal,
}: InstallRulesProps): Promise<InstallMigrationRulesResponse> => {
return KibanaServices.get().http.fetch<InstallMigrationRulesResponse>(
replaceParams(SIEM_RULE_MIGRATION_INSTALL_PATH, { migration_id: migrationId }),
{
method: 'POST',
version: '1',
body: JSON.stringify(ids),
signal,
}
);
};
export const installTranslatedMigrationRules = async ({
migrationId,
signal,
}: InstallTranslatedRulesProps): Promise<InstallTranslatedMigrationRulesResponse> => {
return KibanaServices.get().http.fetch<InstallTranslatedMigrationRulesResponse>(
replaceParams(SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH, { migration_id: migrationId }),
{
method: 'POST',
version: '1',
signal,
}
);
};

View file

@ -6,14 +6,15 @@
*/
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } 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 { 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';
export const useGetRuleMigrationsQuery = (
export const useGetMigrationRulesQuery = (
migrationId: string,
options?: UseQueryOptions<GetRuleMigrationResponse>
) => {
@ -31,3 +32,23 @@ export const useGetRuleMigrationsQuery = (
}
);
};
/**
* We should use this hook to invalidate the rule migrations cache. For
* example, rule migrations mutations, like installing a rule, should lead to cache invalidation.
*
* @returns A rule migrations cache invalidation callback
*/
export const useInvalidateGetMigrationRulesQuery = (migrationId: string) => {
const queryClient = useQueryClient();
const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, {
migration_id: migrationId,
});
return useCallback(() => {
queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_PATH], {
refetchType: 'active',
});
}, [SPECIFIC_MIGRATION_PATH, queryClient]);
};

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 { UseMutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { InstallTranslatedMigrationRulesResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH } from '../../../../../common/siem_migrations/constants';
import { installTranslatedMigrationRules } from '../api';
import { useInvalidateGetMigrationRulesQuery } from './use_get_migration_rules_query';
export const INSTALL_ALL_MIGRATION_RULES_MUTATION_KEY = [
'POST',
SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH,
];
export const useInstallAllMigrationRulesMutation = (
migrationId: string,
options?: UseMutationOptions<InstallTranslatedMigrationRulesResponse, Error>
) => {
const invalidateGetRuleMigrationsQuery = useInvalidateGetMigrationRulesQuery(migrationId);
return useMutation<InstallTranslatedMigrationRulesResponse, Error>(
() => installTranslatedMigrationRules({ migrationId }),
{
...options,
mutationKey: INSTALL_ALL_MIGRATION_RULES_MUTATION_KEY,
onSettled: (...args) => {
invalidateGetRuleMigrationsQuery();
if (options?.onSettled) {
options.onSettled(...args);
}
},
}
);
};

View file

@ -0,0 +1,36 @@
/*
* 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 { UseMutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
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 { installMigrationRules } from '../api';
import { useInvalidateGetMigrationRulesQuery } from './use_get_migration_rules_query';
export const INSTALL_MIGRATION_RULES_MUTATION_KEY = ['POST', SIEM_RULE_MIGRATION_INSTALL_PATH];
export const useInstallMigrationRulesMutation = (
migrationId: string,
options?: UseMutationOptions<InstallMigrationRulesResponse, Error, string[]>
) => {
const invalidateGetRuleMigrationsQuery = useInvalidateGetMigrationRulesQuery(migrationId);
return useMutation<InstallMigrationRulesResponse, Error, string[]>(
(ids: string[]) => installMigrationRules({ migrationId, ids }),
{
...options,
mutationKey: INSTALL_MIGRATION_RULES_MUTATION_KEY,
onSettled: (...args) => {
invalidateGetRuleMigrationsQuery();
if (options?.onSettled) {
options.onSettled(...args);
}
},
}
);
};

View file

@ -0,0 +1,68 @@
/*
* 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import * as i18n from './translations';
export interface BulkActionsProps {
isTableLoading: boolean;
numberOfTranslatedRules: number;
numberOfSelectedRules: number;
installTranslatedRule?: () => void;
installSelectedRule?: () => void;
}
/**
* Collection of buttons to perform bulk actions on migration rules within the SIEM Rules Migrations table.
*/
export const BulkActions: React.FC<BulkActionsProps> = React.memo(
({
isTableLoading,
numberOfTranslatedRules,
numberOfSelectedRules,
installTranslatedRule,
installSelectedRule,
}) => {
const showInstallTranslatedRulesButton = numberOfTranslatedRules > 0;
const showInstallSelectedRulesButton =
showInstallTranslatedRulesButton && numberOfSelectedRules > 0;
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
{showInstallSelectedRulesButton ? (
<EuiFlexItem grow={false}>
<EuiButton
onClick={installSelectedRule}
disabled={isTableLoading}
data-test-subj="installSelectedRulesButton"
aria-label={i18n.INSTALL_SELECTED_ARIA_LABEL}
>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
{isTableLoading && <EuiLoadingSpinner size="s" />}
</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}
</EuiFlexGroup>
);
}
);
BulkActions.displayName = 'BulkActions';

View file

@ -8,15 +8,10 @@
import { EuiFlexGroup } from '@elastic/eui';
import type { Dispatch, SetStateAction } from 'react';
import React, { useCallback } from 'react';
import styled from 'styled-components';
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';
const FilterWrapper = styled(EuiFlexGroup)`
margin-bottom: ${({ theme }) => theme.eui.euiSizeM};
`;
export interface FiltersComponentProps {
/**
* Currently selected table filter
@ -45,13 +40,13 @@ const FiltersComponent: React.FC<FiltersComponentProps> = ({ filterOptions, setF
);
return (
<FilterWrapper gutterSize="m" justifyContent="flexEnd" wrap>
<EuiFlexGroup gutterSize="m" justifyContent="flexEnd" wrap>
<RuleSearchField
initialValue={filterOptions.filter}
onSearch={handleOnSearch}
placeholder={i18n.SEARCH_PLACEHOLDER}
/>
</FilterWrapper>
</EuiFlexGroup>
);
};

View file

@ -13,8 +13,9 @@ import {
EuiSkeletonText,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';
import React, { useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import {
@ -24,32 +25,26 @@ import {
import { NoItemsMessage } from './no_items_message';
import { Filters } from './filters';
import { useRulesTableColumns } from '../../hooks/use_rules_table_columns';
import { useGetRuleMigrationsQuery } from '../../api/hooks/use_get_rule_migrations';
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 { BulkActions } from './bulk_actions';
export interface RulesTableComponentProps {
/**
* Selected rule migration id
*/
migrationId: string;
/**
* Opens the flyout with the details of the rule migration
* @param rule Rule migration
* @returns
*/
openRulePreview: (rule: RuleMigration) => void;
}
/**
* Table Component for displaying SIEM rules migrations
*/
const RulesTableComponent: React.FC<RulesTableComponentProps> = ({
migrationId,
openRulePreview,
}) => {
const { data: ruleMigrations, isLoading } = useGetRuleMigrationsQuery(migrationId);
const RulesTableComponent: React.FC<RulesTableComponentProps> = ({ migrationId }) => {
const { data: ruleMigrations, isLoading: isDataLoading } = useGetMigrationRules(migrationId);
const [selectedRuleMigrations, setSelectedRuleMigrations] = useState<RuleMigration[]>([]);
@ -62,10 +57,60 @@ const RulesTableComponent: React.FC<RulesTableComponentProps> = ({
ruleMigrations: ruleMigrations ?? [],
});
const shouldShowProgress = isLoading;
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 [isTableLoading, setTableLoading] = useState(false);
const installSingleRule = useCallback(
async (migrationRule: RuleMigration, enable?: boolean) => {
setTableLoading(true);
try {
await installMigrationRules([migrationRule.id]);
} finally {
setTableLoading(false);
}
},
[installMigrationRules]
);
const installTranslatedRules = useCallback(
async (enable?: boolean) => {
setTableLoading(true);
try {
await installAllMigrationRules();
} finally {
setTableLoading(false);
}
},
[installAllMigrationRules]
);
const ruleActionsFactory = useCallback(
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
// TODO: Add flyout action buttons
return null;
},
[]
);
const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
ruleActionsFactory,
});
const shouldShowProgress = isDataLoading;
const rulesColumns = useRulesTableColumns({
openRulePreview,
disableActions: isTableLoading,
openMigrationRulePreview: openRulePreview,
installMigrationRule: installSingleRule,
});
return (
@ -79,7 +124,7 @@ const RulesTableComponent: React.FC<RulesTableComponentProps> = ({
/>
)}
<EuiSkeletonLoading
isLoading={isLoading}
isLoading={isDataLoading}
loadingContent={
<>
<EuiSkeletonTitle />
@ -91,13 +136,22 @@ const RulesTableComponent: React.FC<RulesTableComponentProps> = ({
<NoItemsMessage />
) : (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m" justifyContent="flexEnd" wrap>
<EuiFlexItem>
<Filters filterOptions={filterOptions} setFilterOptions={setFilterOptions} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<BulkActions
isTableLoading={isDataLoading || isTableLoading}
numberOfTranslatedRules={numberOfTranslatedRules}
numberOfSelectedRules={0}
installTranslatedRule={installTranslatedRules}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiInMemoryTable
loading={isTableLoading}
items={filteredRuleMigrations}
sorting
pagination={{
@ -117,6 +171,7 @@ const RulesTableComponent: React.FC<RulesTableComponentProps> = ({
)
}
/>
{rulePreviewFlyout}
</>
);
};

View file

@ -34,3 +34,32 @@ export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate(
defaultMessage: 'Go back to SIEM Migrations',
}
);
export const INSTALL_SELECTED_RULES = (numberOfSelectedRules: number) => {
return i18n.translate('xpack.securitySolution.siemMigrations.rules.table.installSelectedRules', {
defaultMessage: 'Install selected ({numberOfSelectedRules})',
values: { numberOfSelectedRules },
});
};
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_SELECTED_ARIA_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.installSelectedButtonAriaLabel',
{
defaultMessage: 'Install selected translated rules',
}
);
export const INSTALL_ALL_ARIA_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.installAllButtonAriaLabel',
{
defaultMessage: 'Install all translated rules',
}
);

View file

@ -0,0 +1,111 @@
/*
* 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 { EuiLink } from '@elastic/eui';
import { getRuleDetailsUrl } from '../../../../common/components/link_to';
import { useKibana } from '../../../../common/lib/kibana';
import { APP_UI_ID, SecurityPageName } from '../../../../../common';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
interface ActionNameProps {
disableActions?: boolean;
migrationRule: RuleMigration;
openMigrationRulePreview: (migrationRule: RuleMigration) => void;
installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void;
}
const ActionName = ({
disableActions,
migrationRule,
openMigrationRulePreview,
installMigrationRule,
}: ActionNameProps) => {
const { navigateToApp } = useKibana().services.application;
if (migrationRule.elastic_rule?.id) {
const ruleId = migrationRule.elastic_rule.id;
return (
<EuiLink
disabled={disableActions}
onClick={() => {
navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.rules,
path: getRuleDetailsUrl(ruleId),
});
}}
data-test-subj="viewRule"
>
{i18n.ACTIONS_VIEW_LABEL}
</EuiLink>
);
}
if (migrationRule.status === 'failed') {
return (
<EuiLink disabled={disableActions} onClick={() => {}} data-test-subj="restartRule">
{i18n.ACTIONS_RESTART_LABEL}
</EuiLink>
);
}
if (migrationRule.translation_result === 'full') {
return (
<EuiLink
disabled={disableActions}
onClick={() => {
installMigrationRule(migrationRule);
}}
data-test-subj="installRule"
>
{i18n.ACTIONS_INSTALL_LABEL}
</EuiLink>
);
}
return (
<EuiLink
disabled={disableActions}
onClick={() => {
openMigrationRulePreview(migrationRule);
}}
data-test-subj="editRule"
>
{i18n.ACTIONS_EDIT_LABEL}
</EuiLink>
);
};
interface CreateActionsColumnProps {
disableActions?: boolean;
openMigrationRulePreview: (migrationRule: RuleMigration) => void;
installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void;
}
export const createActionsColumn = ({
disableActions,
openMigrationRulePreview,
installMigrationRule,
}: CreateActionsColumnProps): TableColumn => {
return {
field: 'elastic_rule',
name: i18n.COLUMN_ACTIONS,
render: (value: RuleMigration['elastic_rule'], migrationRule: RuleMigration) => {
return (
<ActionName
disableActions={disableActions}
migrationRule={migrationRule}
openMigrationRulePreview={openMigrationRulePreview}
installMigrationRule={installMigrationRule}
/>
);
},
width: '10%',
align: 'center',
};
};

View file

@ -5,11 +5,7 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import type { EuiBasicTableColumn } from '@elastic/eui';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
export const COLUMN_STATUS = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.columns.statusTitle',
{
defaultMessage: 'Status',
}
);
export type TableColumn = EuiBasicTableColumn<RuleMigration>;

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
export * from './constants';
export const DEFAULT_TRANSLATION_RISK_SCORE = 21;
export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low';
export * from './actions';
export * from './name';
export * from './risk_score';
export * from './severity';
export * from './status';

View file

@ -0,0 +1,49 @@
/*
* 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 { EuiLink } from '@elastic/eui';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
interface NameProps {
name: string;
rule: RuleMigration;
openMigrationRulePreview: (rule: RuleMigration) => void;
}
const Name = ({ name, rule, openMigrationRulePreview }: NameProps) => {
return (
<EuiLink
onClick={() => {
openMigrationRulePreview(rule);
}}
data-test-subj="ruleName"
>
{name}
</EuiLink>
);
};
export const createNameColumn = ({
openMigrationRulePreview,
}: {
openMigrationRulePreview: (rule: RuleMigration) => void;
}): TableColumn => {
return {
field: 'original_rule.title',
name: i18n.COLUMN_NAME,
render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => (
<Name name={value} rule={rule} openMigrationRulePreview={openMigrationRulePreview} />
),
sortable: true,
truncateText: true,
width: '40%',
align: 'left',
};
};

View file

@ -0,0 +1,27 @@
/*
* 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 { EuiText } from '@elastic/eui';
import { DEFAULT_TRANSLATION_RISK_SCORE } from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
import type { TableColumn } from './constants';
export const createRiskScoreColumn = (): TableColumn => {
return {
field: 'risk_score',
name: i18n.COLUMN_RISK_SCORE,
render: () => (
<EuiText data-test-subj="riskScore" size="s">
{DEFAULT_TRANSLATION_RISK_SCORE}
</EuiText>
),
sortable: true,
truncateText: true,
width: '75px',
};
};

View file

@ -0,0 +1,27 @@
/*
* 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 type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { DEFAULT_TRANSLATION_SEVERITY } from '../../../../../common/siem_migrations/constants';
import { getNormalizedSeverity } from '../../../../detection_engine/rule_management_ui/components/rules_table/helpers';
import { SeverityBadge } from '../../../../common/components/severity_badge';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { TableColumn } from './constants';
import * as i18n from './translations';
export const createSeverityColumn = (): TableColumn => {
return {
field: 'elastic_rule.severity',
name: i18n.COLUMN_SEVERITY,
render: (value?: Severity) => <SeverityBadge value={value ?? DEFAULT_TRANSLATION_SEVERITY} />,
sortable: ({ elastic_rule: elasticRule }: RuleMigration) =>
getNormalizedSeverity((elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY),
truncateText: true,
width: '12%',
};
};

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
import { StatusBadge } from '../status_badge';
export const createStatusColumn = (): TableColumn => {
return {
field: 'translation_result',
name: i18n.COLUMN_STATUS,
render: (value: RuleMigration['translation_result'], rule: RuleMigration) => (
<StatusBadge value={value} installedRuleId={rule.elastic_rule?.id} />
),
sortable: false,
truncateText: true,
width: '12%',
};
};

View file

@ -0,0 +1,71 @@
/*
* 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 COLUMN_ACTIONS = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsLabel',
{
defaultMessage: 'Actions',
}
);
export const ACTIONS_VIEW_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsViewLabel',
{
defaultMessage: 'View',
}
);
export const ACTIONS_EDIT_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsEditLabel',
{
defaultMessage: 'Edit',
}
);
export const ACTIONS_INSTALL_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsInstallLabel',
{
defaultMessage: 'Install',
}
);
export const ACTIONS_RESTART_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsRestartLabel',
{
defaultMessage: 'Restart',
}
);
export const COLUMN_NAME = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.nameLabel',
{
defaultMessage: 'Name',
}
);
export const COLUMN_STATUS = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.statusLabel',
{
defaultMessage: 'Status',
}
);
export const COLUMN_RISK_SCORE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.riskScoreLabel',
{
defaultMessage: 'Risk score',
}
);
export const COLUMN_SEVERITY = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tableColumn.severityLabel',
{
defaultMessage: 'Severity',
}
);

View file

@ -25,6 +25,10 @@ import {
} from '@elastic/eui';
import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui';
import {
DEFAULT_TRANSLATION_RISK_SCORE,
DEFAULT_TRANSLATION_SEVERITY,
} from '../../../../../common/siem_migrations/constants';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import {
RuleOverviewTab,
@ -41,10 +45,6 @@ import {
LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS,
} from './constants';
import { TranslationTab } from './translation_tab';
import {
DEFAULT_TRANSLATION_RISK_SCORE,
DEFAULT_TRANSLATION_SEVERITY,
} from '../../utils/constants';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflow {

View file

@ -5,103 +5,38 @@
* 2.0.
*/
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiText, EuiLink } from '@elastic/eui';
import React, { useMemo } from 'react';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { useMemo } from 'react';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import { SeverityBadge } from '../../../common/components/severity_badge';
import * as rulesI18n from '../../../detections/pages/detection_engine/rules/translations';
import * as i18n from './translations';
import { getNormalizedSeverity } from '../../../detection_engine/rule_management_ui/components/rules_table/helpers';
import { StatusBadge } from '../components/status_badge';
import { DEFAULT_TRANSLATION_RISK_SCORE, DEFAULT_TRANSLATION_SEVERITY } from '../utils/constants';
export type TableColumn = EuiBasicTableColumn<RuleMigration>;
interface RuleNameProps {
name: string;
rule: RuleMigration;
openRulePreview: (rule: RuleMigration) => void;
}
const RuleName = ({ name, rule, openRulePreview }: RuleNameProps) => {
return (
<EuiLink
onClick={() => {
openRulePreview(rule);
}}
data-test-subj="ruleName"
>
{name}
</EuiLink>
);
};
const createRuleNameColumn = ({
openRulePreview,
}: {
openRulePreview: (rule: RuleMigration) => void;
}): TableColumn => {
return {
field: 'original_rule.title',
name: rulesI18n.COLUMN_RULE,
render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => (
<RuleName name={value} rule={rule} openRulePreview={openRulePreview} />
),
sortable: true,
truncateText: true,
width: '40%',
align: 'left',
};
};
const STATUS_COLUMN: TableColumn = {
field: 'translation_result',
name: i18n.COLUMN_STATUS,
render: (value: RuleMigration['translation_result'], rule: RuleMigration) => (
<StatusBadge value={value} installedRuleId={rule.elastic_rule?.id} />
),
sortable: false,
truncateText: true,
width: '12%',
};
import type { TableColumn } from '../components/rules_table_columns';
import {
createActionsColumn,
createNameColumn,
createRiskScoreColumn,
createSeverityColumn,
createStatusColumn,
} from '../components/rules_table_columns';
export const useRulesTableColumns = ({
openRulePreview,
disableActions,
openMigrationRulePreview,
installMigrationRule,
}: {
openRulePreview: (rule: RuleMigration) => void;
disableActions?: boolean;
openMigrationRulePreview: (rule: RuleMigration) => void;
installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void;
}): TableColumn[] => {
return useMemo(
() => [
createRuleNameColumn({ openRulePreview }),
STATUS_COLUMN,
{
field: 'risk_score',
name: rulesI18n.COLUMN_RISK_SCORE,
render: () => (
<EuiText data-test-subj="riskScore" size="s">
{DEFAULT_TRANSLATION_RISK_SCORE}
</EuiText>
),
sortable: true,
truncateText: true,
width: '75px',
},
{
field: 'elastic_rule.severity',
name: rulesI18n.COLUMN_SEVERITY,
render: (value?: Severity) => (
<SeverityBadge value={value ?? DEFAULT_TRANSLATION_SEVERITY} />
),
sortable: ({ elastic_rule: elasticRule }: RuleMigration) =>
getNormalizedSeverity(
(elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY
),
truncateText: true,
width: '12%',
},
createNameColumn({ openMigrationRulePreview }),
createStatusColumn(),
createRiskScoreColumn(),
createSeverityColumn(),
createActionsColumn({
disableActions,
openMigrationRulePreview,
installMigrationRule,
}),
],
[openRulePreview]
[disableActions, installMigrationRule, openMigrationRulePreview]
);
};

View file

@ -0,0 +1,22 @@
/*
* 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 GET_MIGRATION_RULES_FAILURE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.getMigrationRulesFailDescription',
{
defaultMessage: 'Failed to fetch migration rules',
}
);
export const INSTALL_MIGRATION_RULES_FAILURE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.installMigrationRulesFailDescription',
{
defaultMessage: 'Failed to install migration rules',
}
);

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 { useGetMigrationRulesQuery } from '../api/hooks/use_get_migration_rules_query';
import * as i18n from './translations';
export const useGetMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
return useGetMigrationRulesQuery(migrationId, {
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 { useInstallAllMigrationRulesMutation } from '../api/hooks/use_install_all_migration_rules_mutation';
import * as i18n from './translations';
export const useInstallAllMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
return useInstallAllMigrationRulesMutation(migrationId, {
onError: (error) => {
addError(error, { title: i18n.INSTALL_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 { useInstallMigrationRulesMutation } from '../api/hooks/use_install_migration_rules_mutation';
import * as i18n from './translations';
export const useInstallMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
return useInstallMigrationRulesMutation(migrationId, {
onError: (error) => {
addError(error, { title: i18n.INSTALL_MIGRATION_RULES_FAILURE });
},
});
};

View file

@ -5,12 +5,11 @@
* 2.0.
*/
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui';
import type { RouteComponentProps } from 'react-router-dom';
import { useNavigation } from '../../../common/lib/kibana';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import { HeaderPage } from '../../../common/components/header_page';
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
import { SecurityPageName } from '../../../app/types';
@ -20,7 +19,6 @@ import { RulesTable } from '../components/rules_table';
import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/callouts/need_admin_for_update_callout';
import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout';
import { HeaderButtons } from '../components/header_buttons';
import { useRulePreviewFlyout } from '../hooks/use_rule_preview_flyout';
import { UnknownMigration } from '../components/unknown_migration';
import { useLatestStats } from '../hooks/use_latest_stats';
@ -66,24 +64,12 @@ export const RulesPage: React.FC<RulesMigrationPageProps> = React.memo(
navigateTo({ deepLinkId: SecurityPageName.siemMigrationsRules, path: selectedId });
};
const ruleActionsFactory = useCallback(
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
// TODO: Add flyout action buttons
return null;
},
[]
);
const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
ruleActionsFactory,
});
const content = useMemo(() => {
if (!migrationId || !migrationsIds.includes(migrationId)) {
return <UnknownMigration />;
}
return <RulesTable migrationId={migrationId} openRulePreview={openRulePreview} />;
}, [migrationId, migrationsIds, openRulePreview]);
return <RulesTable migrationId={migrationId} />;
}, [migrationId, migrationsIds]);
return (
<>
@ -108,7 +94,6 @@ export const RulesPage: React.FC<RulesMigrationPageProps> = React.memo(
}
loadedContent={content}
/>
{rulePreviewFlyout}
</SecuritySolutionPageWrapper>
</>
);

View file

@ -11,3 +11,14 @@ export interface RuleMigrationTask extends RuleMigrationTaskStats {
/** The sequential number of the migration */
number: number;
}
export interface InstallRulesProps {
migrationId: string;
ids: string[];
signal?: AbortSignal;
}
export interface InstallTranslatedRulesProps {
migrationId: string;
signal?: AbortSignal;
}

View file

@ -38,7 +38,7 @@ export const registerSiemRuleMigrationsGetRoute = (
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const migrationRules = await ruleMigrationsClient.data.rules.get(migrationId);
const migrationRules = await ruleMigrationsClient.data.rules.get({ migrationId });
return res.ok({ body: migrationRules });
} catch (err) {

View file

@ -17,6 +17,8 @@ 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';
export const registerSiemRuleMigrationsRoutes = (
router: SecuritySolutionPluginRouter,
@ -30,6 +32,8 @@ export const registerSiemRuleMigrationsRoutes = (
registerSiemRuleMigrationsRetryRoute(router, logger);
registerSiemRuleMigrationsStatsRoute(router, logger);
registerSiemRuleMigrationsStopRoute(router, logger);
registerSiemRuleMigrationsInstallRoute(router, logger);
registerSiemRuleMigrationsInstallTranslatedRoute(router, logger);
registerSiemRuleMigrationsResourceUpsertRoute(router, logger);
registerSiemRuleMigrationsResourceGetRoute(router, logger);

View file

@ -0,0 +1,69 @@
/*
* 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 { 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';
export const registerSiemRuleMigrationsInstallRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.post({
path: SIEM_RULE_MIGRATION_INSTALL_PATH,
access: 'internal',
security: { authz: { requiredPrivileges: ['securitySolution'] } },
})
.addVersion(
{
version: '1',
validate: {
request: {
params: buildRouteValidationWithZod(InstallMigrationRulesRequestParams),
body: buildRouteValidationWithZod(InstallMigrationRulesRequestBody),
},
},
},
withLicense(
async (context, req, res): Promise<IKibanaResponse<InstallMigrationRulesResponse>> => {
const { migration_id: migrationId } = req.params;
const ids = req.body;
try {
const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
const securitySolutionContext = ctx.securitySolution;
const savedObjectsClient = ctx.core.savedObjects.client;
const rulesClient = ctx.alerting.getRulesClient();
await installTranslated({
migrationId,
ids,
securitySolutionContext,
savedObjectsClient,
rulesClient,
logger,
});
return res.ok({ body: { installed: true } });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });
}
}
)
);
};

View file

@ -0,0 +1,67 @@
/*
* 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 { 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,
logger: Logger
) => {
router.versioned
.post({
path: SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH,
access: 'internal',
security: { authz: { requiredPrivileges: ['securitySolution'] } },
})
.addVersion(
{
version: '1',
validate: {
request: {
params: buildRouteValidationWithZod(InstallTranslatedMigrationRulesRequestParams),
},
},
},
withLicense(
async (
context,
req,
res
): Promise<IKibanaResponse<InstallTranslatedMigrationRulesResponse>> => {
const { migration_id: migrationId } = req.params;
try {
const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
const securitySolutionContext = ctx.securitySolution;
const savedObjectsClient = ctx.core.savedObjects.client;
const rulesClient = ctx.alerting.getRulesClient();
await installTranslated({
migrationId,
securitySolutionContext,
savedObjectsClient,
rulesClient,
logger,
});
return res.ok({ body: { installed: true } });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });
}
}
)
);
};

View file

@ -0,0 +1,223 @@
/*
* 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 { Logger, SavedObjectsClientContract } from '@kbn/core/server';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import {
DEFAULT_TRANSLATION_RISK_SCORE,
DEFAULT_TRANSLATION_SEVERITY,
} from '../../../../../../common/siem_migrations/constants';
import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..';
import { createPrebuiltRuleObjectsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client';
import { performTimelinesInstallation } from '../../../../detection_engine/prebuilt_rules/logic/perform_timelines_installation';
import { createPrebuiltRules } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/create_prebuilt_rules';
import type { PrebuiltRuleAsset } from '../../../../detection_engine/prebuilt_rules';
import { getRuleGroups } from '../../../../detection_engine/prebuilt_rules/model/rule_groups/get_rule_groups';
import { fetchRuleVersionsTriad } from '../../../../detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad';
import { createPrebuiltRuleAssetsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import type { IDetectionRulesClient } from '../../../../detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface';
import type { RuleCreateProps } from '../../../../../../common/api/detection_engine';
import type { UpdateRuleMigrationInput } from '../../data/rule_migrations_data_rules_client';
import type { StoredRuleMigration } from '../../types';
const installPrebuiltRules = async (
rulesToInstall: StoredRuleMigration[],
securitySolutionContext: SecuritySolutionApiRequestHandlerContext,
rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract,
detectionRulesClient: IDetectionRulesClient
): Promise<UpdateRuleMigrationInput[]> => {
const ruleAssetsClient = createPrebuiltRuleAssetsClient(savedObjectsClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const ruleVersionsMap = await fetchRuleVersionsTriad({
ruleAssetsClient,
ruleObjectsClient,
});
const { currentRules, installableRules } = getRuleGroups(ruleVersionsMap);
const rulesToUpdate: UpdateRuleMigrationInput[] = [];
const assetsToInstall: PrebuiltRuleAsset[] = [];
rulesToInstall.forEach((ruleToInstall) => {
// If prebuilt rule has already been install, then just update migration rule with the installed rule id
const installedRule = currentRules.find(
(rule) => rule.rule_id === ruleToInstall.elastic_rule?.prebuilt_rule_id
);
if (installedRule) {
rulesToUpdate.push({
id: ruleToInstall.id,
elastic_rule: {
id: installedRule.id,
},
});
return;
}
// If prebuilt rule is not installed, then keep reference to install it
const installableRule = installableRules.find(
(rule) => rule.rule_id === ruleToInstall.elastic_rule?.prebuilt_rule_id
);
if (installableRule) {
assetsToInstall.push(installableRule);
}
});
// Filter out any duplicates which can occur when multiple translated rules matched the same prebuilt rule
const filteredAssetsToInstall = assetsToInstall.filter(
(value, index, self) => index === self.findIndex((rule) => rule.rule_id === value.rule_id)
);
// TODO: we need to do an error handling which can happen during the rule installation
const { results: installedRules } = await createPrebuiltRules(
detectionRulesClient,
filteredAssetsToInstall
);
await performTimelinesInstallation(securitySolutionContext);
installedRules.forEach((installedRule) => {
const rules = rulesToInstall.filter(
(rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.result.rule_id
);
rules.forEach((prebuiltRule) => {
rulesToUpdate.push({
id: prebuiltRule.id,
elastic_rule: {
id: installedRule.result.id,
},
});
});
});
return rulesToUpdate;
};
const installCustomRules = async (
rulesToInstall: StoredRuleMigration[],
detectionRulesClient: IDetectionRulesClient,
logger: Logger
): Promise<UpdateRuleMigrationInput[]> => {
const rulesToUpdate: UpdateRuleMigrationInput[] = [];
await Promise.all(
rulesToInstall.map(async (rule) => {
if (!rule.elastic_rule?.query || !rule.elastic_rule?.description) {
return;
}
try {
const payloadRule: RuleCreateProps = {
type: 'esql',
language: 'esql',
query: rule.elastic_rule.query,
name: rule.elastic_rule.title,
description: rule.elastic_rule.description,
severity: DEFAULT_TRANSLATION_SEVERITY,
risk_score: DEFAULT_TRANSLATION_RISK_SCORE,
};
const createdRule = await detectionRulesClient.createCustomRule({
params: payloadRule,
});
rulesToUpdate.push({
id: rule.id,
elastic_rule: {
id: createdRule.id,
},
});
} catch (err) {
// TODO: we need to do an error handling which can happen during the rule creation
logger.debug(`Could not create a rule because of error: ${JSON.stringify(err)}`);
}
})
);
return rulesToUpdate;
};
interface InstallTranslatedProps {
/**
* The migration id
*/
migrationId: string;
/**
* If specified, then installable translated rules in theThe list will be installed,
* otherwise all installable translated rules will be installed.
*/
ids?: string[];
/**
* The security solution context
*/
securitySolutionContext: SecuritySolutionApiRequestHandlerContext;
/**
* The rules client to create rules
*/
rulesClient: RulesClient;
/**
* The saved objects client
*/
savedObjectsClient: SavedObjectsClientContract;
/**
* The logger
*/
logger: Logger;
}
export const installTranslated = async ({
migrationId,
ids,
securitySolutionContext,
rulesClient,
savedObjectsClient,
logger,
}: InstallTranslatedProps) => {
const detectionRulesClient = securitySolutionContext.getDetectionRulesClient();
const ruleMigrationsClient = securitySolutionContext.getSiemRuleMigrationsClient();
const rulesToInstall = await ruleMigrationsClient.data.rules.get({
migrationId,
ids,
installable: true,
});
const { customRulesToInstall, prebuiltRulesToInstall } = rulesToInstall.reduce(
(acc, item) => {
if (item.elastic_rule?.prebuilt_rule_id) {
acc.prebuiltRulesToInstall.push(item);
} else {
acc.customRulesToInstall.push(item);
}
return acc;
},
{ customRulesToInstall: [], prebuiltRulesToInstall: [] } as {
customRulesToInstall: StoredRuleMigration[];
prebuiltRulesToInstall: StoredRuleMigration[];
}
);
const updatedPrebuiltRules = await installPrebuiltRules(
prebuiltRulesToInstall,
securitySolutionContext,
rulesClient,
savedObjectsClient,
detectionRulesClient
);
const updatedCustomRules = await installCustomRules(
customRulesToInstall,
detectionRulesClient,
logger
);
const rulesToUpdate: UpdateRuleMigrationInput[] = [
...updatedPrebuiltRules,
...updatedCustomRules,
];
if (rulesToUpdate.length) {
await ruleMigrationsClient.data.rules.update(rulesToUpdate);
}
};

View file

@ -34,6 +34,13 @@ export type UpdateRuleMigrationInput = { elastic_rule?: Partial<ElasticRule> } &
export type RuleMigrationDataStats = Omit<RuleMigrationTaskStats, 'status'>;
export type RuleMigrationAllDataStats = RuleMigrationDataStats[];
export interface RuleMigrationFilterOptions {
migrationId: string;
status?: SiemMigrationStatus | SiemMigrationStatus[];
ids?: string[];
installable?: boolean;
}
/* BULK_MAX_SIZE defines the number to break down the bulk operations by.
* The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed.
*/
@ -101,9 +108,9 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
}
/** Retrieves an array of rule documents of a specific migrations */
async get(migrationId: string): Promise<StoredRuleMigration[]> {
async get(filters: RuleMigrationFilterOptions): Promise<StoredRuleMigration[]> {
const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId);
const query = this.getFilterQuery(filters);
const storedRuleMigrations = await this.esClient
.search<RuleMigration>({ index, query, sort: '_doc' })
@ -123,7 +130,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
*/
async takePending(migrationId: string, size: number): Promise<StoredRuleMigration[]> {
const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId, SiemMigrationStatus.PENDING);
const query = this.getFilterQuery({ migrationId, status: SiemMigrationStatus.PENDING });
const storedRuleMigrations = await this.esClient
.search<RuleMigration>({ index, query, sort: '_doc', size })
@ -202,7 +209,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
{ refresh = false }: { refresh?: boolean } = {}
): Promise<void> {
const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId, statusToQuery);
const query = this.getFilterQuery({ migrationId, status: statusToQuery });
const script = { source: `ctx._source['status'] = '${statusToUpdate}'` };
await this.esClient.updateByQuery({ index, query, script, refresh }).catch((error) => {
this.logger.error(`Error updating rule migrations status: ${error.message}`);
@ -213,7 +220,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
/** Retrieves the stats for the rule migrations with the provided id */
async getStats(migrationId: string): Promise<RuleMigrationDataStats> {
const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId);
const query = this.getFilterQuery({ migrationId });
const aggregations = {
pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } },
processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } },
@ -283,10 +290,12 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
}));
}
private getFilterQuery(
migrationId: string,
status?: SiemMigrationStatus | SiemMigrationStatus[]
): QueryDslQueryContainer {
private getFilterQuery({
migrationId,
status,
ids,
installable,
}: RuleMigrationFilterOptions): QueryDslQueryContainer {
const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }];
if (status) {
if (Array.isArray(status)) {
@ -295,6 +304,20 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
filter.push({ term: { status } });
}
}
if (ids) {
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' } } } },
},
}
);
}
return { bool: { filter } };
}
}

View file

@ -6,6 +6,7 @@
*/
import { JsonOutputParser } from '@langchain/core/output_parsers';
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
import type { ChatModel } from '../../../util/actions_client_chat';
import { filterPrebuiltRules, type PrebuiltRulesMapByName } from '../../../util/prebuilt_rules';
import type { GraphNode } from '../../types';
@ -51,6 +52,7 @@ export const getMatchPrebuiltRuleNode =
prebuilt_rule_id: result.rule.rule_id,
id: result.installedRuleId,
},
translation_result: SiemMigrationRuleTranslationResult.FULL,
};
}
}

View file

@ -107,7 +107,12 @@ import {
InitEntityEngineRequestBodyInput,
} from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/init.gen';
import { InitEntityStoreRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/enable.gen';
import {
InstallMigrationRulesRequestParamsInput,
InstallMigrationRulesRequestBodyInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen';
import { InstallTranslatedMigrationRulesRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { ListEntitiesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/entities/list_entities.gen';
import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen';
import { PatchTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/patch_timelines/patch_timeline_route.gen';
@ -1046,6 +1051,22 @@ finalize it.
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Installs migration rules
*/
installMigrationRules(props: InstallMigrationRulesProps, kibanaSpace: string = 'default') {
return supertest
.post(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}/install', props.params),
kibanaSpace
)
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
/**
* Install and update all Elastic prebuilt detection rules and Timelines.
*/
@ -1070,6 +1091,27 @@ finalize it.
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
/**
* Installs all translated migration rules
*/
installTranslatedMigrationRules(
props: InstallTranslatedMigrationRulesProps,
kibanaSpace: string = 'default'
) {
return supertest
.post(
routeWithNamespace(
replaceParams(
'/internal/siem_migrations/rules/{migration_id}/install_translated',
props.params
),
kibanaSpace
)
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
internalUploadAssetCriticalityRecords(kibanaSpace: string = 'default') {
return supertest
.post(routeWithNamespace('/internal/asset_criticality/upload_csv', kibanaSpace))
@ -1653,9 +1695,16 @@ export interface InitEntityEngineProps {
export interface InitEntityStoreProps {
body: InitEntityStoreRequestBodyInput;
}
export interface InstallMigrationRulesProps {
params: InstallMigrationRulesRequestParamsInput;
body: InstallMigrationRulesRequestBodyInput;
}
export interface InstallPrepackedTimelinesProps {
body: InstallPrepackedTimelinesRequestBodyInput;
}
export interface InstallTranslatedMigrationRulesProps {
params: InstallTranslatedMigrationRulesRequestParamsInput;
}
export interface ListEntitiesProps {
query: ListEntitiesRequestQueryInput;
}