[Rules migration] Add functionality to display matched prebuilt rules details (#11360) (#203035)

## Summary

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

These changes add functionality that allows to display matched prebuilt
rules details.

### New route

There is a new route
`/internal/siem_migrations/rules/{migration_id}/prebuilt_rules` that
will return all prebuilt rules matched by translated rules within a
specific migration.

### UI changes

The rule migration details flyout was updated to display matched
prebuilt rule data in both `Translation` and `Overview` tabs.


https://github.com/user-attachments/assets/3da49653-e0ab-4d8b-892e-dd05cf73743b

### Other changes

Also, as part of this PR, batching of a rule installation/creation was
added.

---------

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-12-08 23:04:39 +01:00 committed by GitHub
parent 194a324b75
commit b3d6d914b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 638 additions and 200 deletions

View file

@ -363,6 +363,8 @@ import type {
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
GetRuleMigrationResponse,
GetRuleMigrationPrebuiltRulesRequestParamsInput,
GetRuleMigrationPrebuiltRulesResponse,
GetRuleMigrationResourcesRequestQueryInput,
GetRuleMigrationResourcesRequestParamsInput,
GetRuleMigrationResourcesResponse,
@ -1431,6 +1433,24 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retrieves all available prebuilt rules (installed and installable)
*/
async getRuleMigrationPrebuiltRules(props: GetRuleMigrationPrebuiltRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationPrebuiltRules`);
return this.kbnClient
.request<GetRuleMigrationPrebuiltRulesResponse>({
path: replaceParams(
'/internal/siem_migrations/rules/{migration_id}/prebuilt_rules',
props.params
),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'GET',
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retrieves resources for an existing SIEM rules migration
*/
@ -2396,6 +2416,9 @@ export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput;
}
export interface GetRuleMigrationPrebuiltRulesProps {
params: GetRuleMigrationPrebuiltRulesRequestParamsInput;
}
export interface GetRuleMigrationResourcesProps {
query: GetRuleMigrationResourcesRequestQueryInput;
params: GetRuleMigrationResourcesRequestParamsInput;

View file

@ -23,6 +23,8 @@ export const SIEM_RULE_MIGRATION_STOP_PATH = `${SIEM_RULE_MIGRATION_PATH}/stop`
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_MIGRATIONS_PREBUILT_RULES_PATH =
`${SIEM_RULE_MIGRATION_PATH}/prebuilt_rules` as const;
export const SIEM_RULE_MIGRATION_RESOURCES_PATH = `${SIEM_RULE_MIGRATION_PATH}/resources` as const;

View file

@ -26,6 +26,7 @@ import {
OriginalRule,
RuleMigration,
RuleMigrationTranslationStats,
PrebuiltRuleVersion,
RuleMigrationResourceData,
RuleMigrationResourceType,
RuleMigrationResource,
@ -76,6 +77,24 @@ export const GetRuleMigrationResponse = z.object({
total: z.number(),
data: z.array(RuleMigration),
});
export type GetRuleMigrationPrebuiltRulesRequestParams = z.infer<
typeof GetRuleMigrationPrebuiltRulesRequestParams
>;
export const GetRuleMigrationPrebuiltRulesRequestParams = z.object({
migration_id: NonEmptyString,
});
export type GetRuleMigrationPrebuiltRulesRequestParamsInput = z.input<
typeof GetRuleMigrationPrebuiltRulesRequestParams
>;
/**
* The map of prebuilt rules, with the rules id as a key
*/
export type GetRuleMigrationPrebuiltRulesResponse = z.infer<
typeof GetRuleMigrationPrebuiltRulesResponse
>;
export const GetRuleMigrationPrebuiltRulesResponse = z.object({}).catchall(PrebuiltRuleVersion);
export type GetRuleMigrationResourcesRequestQuery = z.infer<
typeof GetRuleMigrationResourcesRequestQuery
>;

View file

@ -355,6 +355,33 @@ paths:
204:
description: Indicates the migration id was not found running.
/internal/siem_migrations/rules/{migration_id}/prebuilt_rules:
get:
summary: Retrieves all prebuilt rules for a specific migration
operationId: GetRuleMigrationPrebuiltRules
x-codegen-enabled: true
x-internal: true
description: Retrieves all available prebuilt rules (installed and installable)
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to retrieve prebuilt rules for
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
responses:
200:
description: Indicates prebuilt rules have been retrieved correctly.
content:
application/json:
schema:
type: object
description: The map of prebuilt rules, with the rules id as a key
additionalProperties:
$ref: '../../rule_migration.schema.yaml#/components/schemas/PrebuiltRuleVersion'
# Rule migration resources APIs
/internal/siem_migrations/rules/{migration_id}/resources:

View file

@ -17,6 +17,7 @@
import { z } from '@kbn/zod';
import { NonEmptyString } from '../../api/model/primitives.gen';
import { RuleResponse } from '../../api/detection_engine/model/rule_schema/rule_schemas.gen';
/**
* The original rule vendor identifier.
@ -117,6 +118,21 @@ export const ElasticRule = z.object({
export type ElasticRulePartial = z.infer<typeof ElasticRulePartial>;
export const ElasticRulePartial = ElasticRule.partial();
/**
* The prebuilt rule version.
*/
export type PrebuiltRuleVersion = z.infer<typeof PrebuiltRuleVersion>;
export const PrebuiltRuleVersion = z.object({
/**
* The latest available version of prebuilt rule.
*/
target: RuleResponse,
/**
* The currently installed version of prebuilt rule.
*/
current: RuleResponse.optional(),
});
/**
* The rule translation result.
*/

View file

@ -97,6 +97,19 @@ components:
$ref: '#/components/schemas/ElasticRule'
x-modify: partial
PrebuiltRuleVersion:
type: object
description: The prebuilt rule version.
required:
- target
properties:
target:
description: The latest available version of prebuilt rule.
$ref: '../../../common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleResponse'
current:
description: The currently installed version of prebuilt rule.
$ref: '../../../common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleResponse'
RuleMigration:
description: The rule migration document object.
allOf:

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 { Severity } from '../../api/detection_engine';
import { DEFAULT_TRANSLATION_FIELDS, DEFAULT_TRANSLATION_SEVERITY } from '../constants';
import type { ElasticRule, ElasticRulePartial } from '../model/rule_migration.gen';
export type MigrationPrebuiltRule = ElasticRulePartial &
Required<Pick<ElasticRulePartial, 'title' | 'description' | 'prebuilt_rule_id'>>;
export type MigrationCustomRule = ElasticRulePartial &
Required<Pick<ElasticRulePartial, 'title' | 'description' | 'query' | 'query_language'>>;
export const isMigrationPrebuiltRule = (rule?: ElasticRule): rule is MigrationPrebuiltRule =>
!!(rule?.title && rule?.description && rule?.prebuilt_rule_id);
export const isMigrationCustomRule = (rule?: ElasticRule): rule is MigrationCustomRule =>
!isMigrationPrebuiltRule(rule) &&
!!(rule?.title && rule?.description && rule?.query && rule?.query_language);
export const convertMigrationCustomRuleToSecurityRulePayload = (rule: MigrationCustomRule) => {
return {
type: rule.query_language,
language: rule.query_language,
query: rule.query,
name: rule.title,
description: rule.description,
...DEFAULT_TRANSLATION_FIELDS,
severity: (rule.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY,
};
};

View file

@ -19,6 +19,7 @@ import {
SIEM_RULE_MIGRATION_START_PATH,
SIEM_RULE_MIGRATION_STATS_PATH,
SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH,
} from '../../../../common/siem_migrations/constants';
import type {
CreateRuleMigrationRequestBody,
@ -30,6 +31,7 @@ import type {
InstallMigrationRulesResponse,
StartRuleMigrationRequestBody,
GetRuleMigrationStatsResponse,
GetRuleMigrationPrebuiltRulesResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
export interface GetRuleMigrationStatsParams {
@ -192,3 +194,20 @@ export const installTranslatedMigrationRules = async ({
{ version: '1', signal }
);
};
export interface GetRuleMigrationsPrebuiltRulesParams {
/** `id` of the migration to install rules for */
migrationId: string;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}
/** Retrieves all prebuilt rules matched within a specific migration. */
export const getRuleMigrationsPrebuiltRules = async ({
migrationId,
signal,
}: GetRuleMigrationsPrebuiltRulesParams): Promise<GetRuleMigrationPrebuiltRulesResponse> => {
return KibanaServices.get().http.get<GetRuleMigrationPrebuiltRulesResponse>(
replaceParams(SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH, { migration_id: migrationId }),
{ version: '1', signal }
);
};

View file

@ -24,19 +24,12 @@ import {
} from '@elastic/eui';
import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui';
import {
DEFAULT_TRANSLATION_SEVERITY,
DEFAULT_TRANSLATION_FIELDS,
} from '../../../../../common/siem_migrations/constants';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import {
RuleOverviewTab,
useOverviewTabSections,
} from '../../../../detection_engine/rule_management/components/rule_details/rule_overview_tab';
import {
type RuleResponse,
type Severity,
} from '../../../../../common/api/detection_engine/model/rule_schema';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';
import * as i18n from './translations';
import {
@ -44,6 +37,10 @@ import {
LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS,
} from './constants';
import { TranslationTab } from './translation_tab';
import {
convertMigrationCustomRuleToSecurityRulePayload,
isMigrationCustomRule,
} from '../../../../../common/siem_migrations/rules/utils';
/*
* Fixes tabs to the top and allows the content to scroll.
@ -67,6 +64,7 @@ export const TabContentPadding: FC<PropsWithChildren<unknown>> = ({ children })
interface MigrationRuleDetailsFlyoutProps {
ruleActions?: React.ReactNode;
ruleMigration: RuleMigration;
matchedPrebuiltRule?: RuleResponse;
size?: EuiFlyoutProps['size'];
extraTabs?: EuiTabbedContentTab[];
closeFlyout: () => void;
@ -76,26 +74,21 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
({
ruleActions,
ruleMigration,
matchedPrebuiltRule,
size = 'm',
extraTabs = [],
closeFlyout,
}: MigrationRuleDetailsFlyoutProps) => {
const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections();
const rule: RuleResponse = useMemo(() => {
const esqlLanguage = ruleMigration.elastic_rule?.query_language ?? 'esql';
return {
type: esqlLanguage,
language: esqlLanguage,
name: ruleMigration.elastic_rule?.title,
description: ruleMigration.elastic_rule?.description,
query: ruleMigration.elastic_rule?.query,
...DEFAULT_TRANSLATION_FIELDS,
severity:
(ruleMigration.elastic_rule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY,
} as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter
}, [ruleMigration]);
const rule = useMemo(() => {
if (isMigrationCustomRule(ruleMigration.elastic_rule)) {
return convertMigrationCustomRuleToSecurityRulePayload(
ruleMigration.elastic_rule
) as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter;
}
return matchedPrebuiltRule;
}, [matchedPrebuiltRule, ruleMigration]);
const translationTab: EuiTabbedContentTab = useMemo(
() => ({
@ -103,11 +96,14 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
name: i18n.TRANSLATION_TAB_LABEL,
content: (
<TabContentPadding>
<TranslationTab ruleMigration={ruleMigration} />
<TranslationTab
ruleMigration={ruleMigration}
matchedPrebuiltRule={matchedPrebuiltRule}
/>
</TabContentPadding>
),
}),
[ruleMigration]
[matchedPrebuiltRule, ruleMigration]
);
const overviewTab: EuiTabbedContentTab = useMemo(
@ -116,16 +112,18 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
name: i18n.OVERVIEW_TAB_LABEL,
content: (
<TabContentPadding>
<RuleOverviewTab
rule={rule}
columnWidths={
size === 'l'
? LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS
: DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS
}
expandedOverviewSections={expandedOverviewSections}
toggleOverviewSection={toggleOverviewSection}
/>
{rule && (
<RuleOverviewTab
rule={rule}
columnWidths={
size === 'l'
? LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS
: DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS
}
expandedOverviewSections={expandedOverviewSections}
toggleOverviewSection={toggleOverviewSection}
/>
)}
</TabContentPadding>
),
}),
@ -166,7 +164,9 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2 id={migrationsRulesFlyoutTitleId}>{rule.name}</h2>
<h2 id={migrationsRulesFlyoutTitleId}>
{rule?.name ?? ruleMigration.original_rule.title}
</h2>
</EuiTitle>
<EuiSpacer size="l" />
</EuiFlyoutHeader>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useMemo } from 'react';
import {
EuiAccordion,
EuiBadge,
@ -20,6 +20,7 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/css';
import { FormattedMessage } from '@kbn/i18n-react';
import type { RuleResponse } from '../../../../../../common/api/detection_engine';
import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import { TranslationTabHeader } from './header';
import { MigrationRuleQuery } from './migration_rule_query';
@ -31,82 +32,98 @@ import {
interface TranslationTabProps {
ruleMigration: RuleMigration;
matchedPrebuiltRule?: RuleResponse;
}
export const TranslationTab: React.FC<TranslationTabProps> = React.memo(({ ruleMigration }) => {
const { euiTheme } = useEuiTheme();
export const TranslationTab: React.FC<TranslationTabProps> = React.memo(
({ ruleMigration, matchedPrebuiltRule }) => {
const { euiTheme } = useEuiTheme();
const name = ruleMigration.elastic_rule?.title ?? ruleMigration.original_rule.title;
const originalQuery = ruleMigration.original_rule.query;
const elasticQuery = ruleMigration.elastic_rule?.query ?? 'Prebuilt rule query';
const name = useMemo(
() => ruleMigration.elastic_rule?.title ?? ruleMigration.original_rule.title,
[ruleMigration.elastic_rule?.title, ruleMigration.original_rule.title]
);
const originalQuery = ruleMigration.original_rule.query;
const elasticQuery = useMemo(() => {
let query = ruleMigration.elastic_rule?.query;
if (matchedPrebuiltRule && matchedPrebuiltRule.type !== 'machine_learning') {
query = matchedPrebuiltRule.query;
}
return query ?? '';
}, [matchedPrebuiltRule, ruleMigration.elastic_rule?.query]);
return (
<>
<EuiSpacer size="m" />
<EuiFormRow label={i18n.NAME_LABEL} fullWidth>
<EuiFieldText value={name} fullWidth />
</EuiFormRow>
<EuiSpacer size="m" />
<EuiAccordion
id="translationQueryItem"
buttonContent={<TranslationTabHeader />}
initialIsOpen={true}
>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiSplitPanel.Outer grow hasShadow={false} hasBorder={true}>
<EuiSplitPanel.Inner grow={false} color="subdued" paddingSize="s">
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h2>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.translationDetails.translationTab.statusTitle"
defaultMessage="Translation status"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge
color={convertTranslationResultIntoColor(ruleMigration.translation_result)}
onClick={() => {}}
onClickAriaLabel={'Click to update translation status'}
>
{convertTranslationResultIntoText(ruleMigration.translation_result)}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner grow>
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
<EuiFlexItem grow={1}>
<MigrationRuleQuery
title={i18n.SPLUNK_QUERY_TITLE}
query={originalQuery}
canEdit={false}
return (
<>
<EuiSpacer size="m" />
<EuiFormRow label={i18n.NAME_LABEL} fullWidth>
<EuiFieldText value={name} fullWidth />
</EuiFormRow>
<EuiSpacer size="m" />
<EuiAccordion
id="translationQueryItem"
buttonContent={<TranslationTabHeader />}
initialIsOpen={true}
>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiSplitPanel.Outer grow hasShadow={false} hasBorder={true}>
<EuiSplitPanel.Inner grow={false} color="subdued" paddingSize="s">
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h2>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.translationDetails.translationTab.statusTitle"
defaultMessage="Translation status"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge
color={convertTranslationResultIntoColor(ruleMigration.translation_result)}
onClick={() => {}}
onClickAriaLabel={'Click to update translation status'}
>
{convertTranslationResultIntoText(ruleMigration.translation_result)}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner grow>
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
<EuiFlexItem grow={1}>
<MigrationRuleQuery
title={i18n.SPLUNK_QUERY_TITLE}
query={originalQuery}
canEdit={false}
/>
</EuiFlexItem>
<EuiFlexItem
grow={0}
css={css`
align-self: stretch;
border-right: ${euiTheme.border.thin};
`}
/>
</EuiFlexItem>
<EuiFlexItem
grow={0}
css={css`
align-self: stretch;
border-right: ${euiTheme.border.thin};
`}
/>
<EuiFlexItem grow={1}>
<MigrationRuleQuery
title={i18n.ESQL_TRANSLATION_TITLE}
query={elasticQuery}
canEdit={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiSplitPanel.Inner>
</EuiSplitPanel.Outer>
</EuiFlexItem>
</EuiAccordion>
</>
);
});
<EuiFlexItem grow={1}>
<MigrationRuleQuery
title={
matchedPrebuiltRule
? i18n.PREBUILT_RULE_QUERY_TITLE
: i18n.ESQL_TRANSLATION_TITLE
}
query={elasticQuery}
canEdit={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiSplitPanel.Inner>
</EuiSplitPanel.Outer>
</EuiFlexItem>
</EuiAccordion>
</>
);
}
);
TranslationTab.displayName = 'TranslationTab';

View file

@ -28,6 +28,13 @@ export const SPLUNK_QUERY_TITLE = i18n.translate(
}
);
export const PREBUILT_RULE_QUERY_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.prebuiltRuleQueryTitle',
{
defaultMessage: 'Prebuilt rule query',
}
);
export const ESQL_TRANSLATION_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.esqlTranslationTitle',
{

View file

@ -26,6 +26,7 @@ import { useInstallMigrationRules } from '../../logic/use_install_migration_rule
import { useGetMigrationRules } from '../../logic/use_get_migration_rules';
import { useInstallTranslatedMigrationRules } from '../../logic/use_install_translated_migration_rules';
import { useGetMigrationTranslationStats } from '../../logic/use_get_migration_translation_stats';
import { useGetMigrationPrebuiltRules } from '../../logic/use_get_migration_prebuilt_rules';
import * as logicI18n from '../../logic/translations';
import { BulkActions } from './bulk_actions';
import { SearchField } from './search_field';
@ -53,6 +54,9 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
const { data: translationStats, isLoading: isStatsLoading } =
useGetMigrationTranslationStats(migrationId);
const { data: prebuiltRules = {}, isLoading: isPrebuiltRulesLoading } =
useGetMigrationPrebuiltRules(migrationId);
const {
data: { ruleMigrations, total } = { ruleMigrations: [], total: 0 },
isLoading: isDataLoading,
@ -129,6 +133,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
migrationRuleDetailsFlyout: rulePreviewFlyout,
openMigrationRuleDetails: openRulePreview,
} = useMigrationRuleDetailsFlyout({
prebuiltRules,
ruleActionsFactory,
});
@ -138,6 +143,8 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
installMigrationRule: installSingleRule,
});
const isLoading = isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading;
return (
<>
<EuiSkeletonLoading
@ -159,7 +166,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
</EuiFlexItem>
<EuiFlexItem grow={false}>
<BulkActions
isTableLoading={isStatsLoading || isDataLoading || isTableLoading}
isTableLoading={isLoading}
numberOfTranslatedRules={translationStats?.rules.installable ?? 0}
numberOfSelectedRules={0}
installTranslatedRule={installTranslatedRules}

View file

@ -8,10 +8,15 @@
import type { ReactNode } from 'react';
import React, { useCallback, useState, useMemo } from 'react';
import type { EuiTabbedContentTab } from '@elastic/eui';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleResponse } from '../../../../common/api/detection_engine';
import type {
PrebuiltRuleVersion,
RuleMigration,
} from '../../../../common/siem_migrations/model/rule_migration.gen';
import { MigrationRuleDetailsFlyout } from '../components/rule_details_flyout';
interface UseMigrationRuleDetailsFlyoutParams {
prebuiltRules: Record<string, PrebuiltRuleVersion>;
ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode;
extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[];
}
@ -23,10 +28,12 @@ interface UseMigrationRuleDetailsFlyoutResult {
}
export function useMigrationRuleDetailsFlyout({
prebuiltRules,
extraTabsFactory,
ruleActionsFactory,
}: UseMigrationRuleDetailsFlyoutParams): UseMigrationRuleDetailsFlyoutResult {
const [ruleMigration, setMigrationRuleForPreview] = useState<RuleMigration | undefined>();
const [matchedPrebuiltRule, setMatchedPrebuiltRule] = useState<RuleResponse | undefined>();
const closeMigrationRuleDetails = useCallback(() => setMigrationRuleForPreview(undefined), []);
const ruleActions = useMemo(
() => ruleMigration && ruleActionsFactory(ruleMigration, closeMigrationRuleDetails),
@ -37,19 +44,33 @@ export function useMigrationRuleDetailsFlyout({
[ruleMigration, extraTabsFactory]
);
const openMigrationRuleDetails = useCallback(
(rule: RuleMigration) => {
setMigrationRuleForPreview(rule);
// Find matched prebuilt rule if any and prioritize its installed version
const matchedPrebuiltRuleVersion = rule.elastic_rule?.prebuilt_rule_id
? prebuiltRules[rule.elastic_rule.prebuilt_rule_id]
: undefined;
const prebuiltRule =
matchedPrebuiltRuleVersion?.current ?? matchedPrebuiltRuleVersion?.target;
setMatchedPrebuiltRule(prebuiltRule);
},
[prebuiltRules]
);
return {
migrationRuleDetailsFlyout: ruleMigration && (
<MigrationRuleDetailsFlyout
ruleMigration={ruleMigration}
matchedPrebuiltRule={matchedPrebuiltRule}
size="l"
closeFlyout={closeMigrationRuleDetails}
ruleActions={ruleActions}
extraTabs={extraTabs}
/>
),
openMigrationRuleDetails: useCallback((rule: RuleMigration) => {
setMigrationRuleForPreview(rule);
}, []),
openMigrationRuleDetails,
closeMigrationRuleDetails,
};
}

View file

@ -7,6 +7,13 @@
import { i18n } from '@kbn/i18n';
export const GET_MIGRATION_PREBUILT_RULES_FAILURE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.getMigrationPrebuiltRulesFailDescription',
{
defaultMessage: 'Failed to fetch prebuilt rules',
}
);
export const GET_MIGRATION_RULES_FAILURE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.getMigrationRulesFailDescription',
{

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 { replaceParams } from '@kbn/openapi-common/shared';
import { useQuery } from '@tanstack/react-query';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import type { GetRuleMigrationPrebuiltRulesResponse } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH } from '../../../../common/siem_migrations/constants';
import { getRuleMigrationsPrebuiltRules } from '../api';
import { DEFAULT_QUERY_OPTIONS } from './constants';
import * as i18n from './translations';
export const useGetMigrationPrebuiltRules = (migrationId: string) => {
const { addError } = useAppToasts();
const SPECIFIC_MIGRATIONS_PREBUILT_RULES_PATH = replaceParams(
SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH,
{
migration_id: migrationId,
}
);
return useQuery<GetRuleMigrationPrebuiltRulesResponse>(
['GET', SPECIFIC_MIGRATIONS_PREBUILT_RULES_PATH],
async ({ signal }) => {
return getRuleMigrationsPrebuiltRules({ migrationId, signal });
},
{
...DEFAULT_QUERY_OPTIONS,
onError: (error) => {
addError(error, { title: i18n.GET_MIGRATION_PREBUILT_RULES_FAILURE });
},
}
);
};

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export const MAX_CUSTOM_RULES_TO_CREATE_IN_PARALLEL = 50;
export const MAX_PREBUILT_RULES_TO_FETCH = 10_000 as const;
export const MAX_TRANSLATED_RULES_TO_INSTALL = 10_000 as const;

View file

@ -0,0 +1,73 @@
/*
* 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 { GetRuleMigrationPrebuiltRulesResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationPrebuiltRulesRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH } from '../../../../../common/siem_migrations/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { withLicense } from './util/with_license';
import { getPrebuiltRules, getUniquePrebuiltRuleIds } from './util/prebuilt_rules';
import { MAX_PREBUILT_RULES_TO_FETCH } from './constants';
export const registerSiemRuleMigrationsPrebuiltRulesRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.get({
path: SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH,
access: 'internal',
security: { authz: { requiredPrivileges: ['securitySolution'] } },
})
.addVersion(
{
version: '1',
validate: {
request: {
params: buildRouteValidationWithZod(GetRuleMigrationPrebuiltRulesRequestParams),
},
},
},
withLicense(
async (
context,
req,
res
): Promise<IKibanaResponse<GetRuleMigrationPrebuiltRulesResponse>> => {
const { migration_id: migrationId } = req.params;
try {
const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const savedObjectsClient = ctx.core.savedObjects.client;
const rulesClient = await ctx.alerting.getRulesClient();
const result = await ruleMigrationsClient.data.rules.get(migrationId, {
filters: {
prebuilt: true,
},
from: 0,
size: MAX_PREBUILT_RULES_TO_FETCH,
});
const prebuiltRulesIds = getUniquePrebuiltRuleIds(result.data);
const prebuiltRules = await getPrebuiltRules(
rulesClient,
savedObjectsClient,
prebuiltRulesIds
);
return res.ok({ body: prebuiltRules });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });
}
}
)
);
};

View file

@ -20,6 +20,7 @@ import { registerSiemRuleMigrationsResourceGetRoute } from './resources/get';
import { registerSiemRuleMigrationsRetryRoute } from './retry';
import { registerSiemRuleMigrationsInstallRoute } from './install';
import { registerSiemRuleMigrationsInstallTranslatedRoute } from './install_translated';
import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rules';
export const registerSiemRuleMigrationsRoutes = (
router: SecuritySolutionPluginRouter,
@ -28,6 +29,7 @@ export const registerSiemRuleMigrationsRoutes = (
registerSiemRuleMigrationsCreateRoute(router, logger);
registerSiemRuleMigrationsUpdateRoute(router, logger);
registerSiemRuleMigrationsStatsAllRoute(router, logger);
registerSiemRuleMigrationsPrebuiltRulesRoute(router, logger);
registerSiemRuleMigrationsGetRoute(router, logger);
registerSiemRuleMigrationsStartRoute(router, logger);
registerSiemRuleMigrationsRetryRoute(router, logger);

View file

@ -7,22 +7,23 @@
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 { initPromisePool } from '../../../../../utils/promise_pool';
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 { RuleResponse } from '../../../../../../common/api/detection_engine';
import type { UpdateRuleMigrationInput } from '../../data/rule_migrations_data_rules_client';
import type { StoredRuleMigration } from '../../types';
import { getPrebuiltRules, getUniquePrebuiltRuleIds } from './prebuilt_rules';
import {
MAX_CUSTOM_RULES_TO_CREATE_IN_PARALLEL,
MAX_TRANSLATED_RULES_TO_INSTALL,
} from '../constants';
import {
convertMigrationCustomRuleToSecurityRulePayload,
isMigrationCustomRule,
} from '../../../../../../common/siem_migrations/rules/utils';
const installPrebuiltRules = async (
rulesToInstall: StoredRuleMigration[],
@ -31,105 +32,90 @@ const installPrebuiltRules = async (
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);
// Get required prebuilt rules
const prebuiltRulesIds = getUniquePrebuiltRuleIds(rulesToInstall);
const prebuiltRules = await getPrebuiltRules(rulesClient, savedObjectsClient, prebuiltRulesIds);
const rulesToUpdate: UpdateRuleMigrationInput[] = [];
const assetsToInstall: PrebuiltRuleAsset[] = [];
rulesToInstall.forEach((ruleToInstall) => {
// If prebuilt rule has already been installed, 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;
const { installed: alreadyInstalledRules, installable } = Object.values(prebuiltRules).reduce(
(acc, item) => {
if (item.current) {
acc.installed.push(item.current);
} else {
acc.installable.push(item.target);
}
return acc;
},
{ installed: [], installable: [] } as {
installed: RuleResponse[];
installable: RuleResponse[];
}
// 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)
);
// Install prebuilt rules
// TODO: we need to do an error handling which can happen during the rule installation
const { results: installedRules } = await createPrebuiltRules(
const { results: newlyInstalledRules } = await createPrebuiltRules(
detectionRulesClient,
filteredAssetsToInstall
installable
);
await performTimelinesInstallation(securitySolutionContext);
const installedRules = [
...alreadyInstalledRules,
...newlyInstalledRules.map((value) => value.result),
];
// Create migration rules updates templates
const rulesToUpdate: UpdateRuleMigrationInput[] = [];
installedRules.forEach((installedRule) => {
const rules = rulesToInstall.filter(
(rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.result.rule_id
const filteredRules = rulesToInstall.filter(
(rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.rule_id
);
rules.forEach((prebuiltRule) => {
rulesToUpdate.push({
id: prebuiltRule.id,
rulesToUpdate.push(
...filteredRules.map(({ id }) => ({
id,
elastic_rule: {
id: installedRule.result.id,
id: installedRule.id,
},
});
});
}))
);
});
return rulesToUpdate;
};
const installCustomRules = async (
export 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) {
const createCustomRulesOutcome = await initPromisePool({
concurrency: MAX_CUSTOM_RULES_TO_CREATE_IN_PARALLEL,
items: rulesToInstall,
executor: async (rule) => {
if (!isMigrationCustomRule(rule.elastic_rule)) {
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)}`);
}
})
);
const payloadRule = convertMigrationCustomRuleToSecurityRulePayload(rule.elastic_rule);
const createdRule = await detectionRulesClient.createPrebuiltRule({
params: payloadRule,
});
rulesToUpdate.push({
id: rule.id,
elastic_rule: {
id: createdRule.id,
},
});
},
});
if (createCustomRulesOutcome.errors) {
// TODO: we need to do an error handling which can happen during the rule creation
logger.debug(
`Failed to create some of the rules because of errors: ${JSON.stringify(
createCustomRulesOutcome.errors
)}`
);
}
return rulesToUpdate;
};
@ -179,6 +165,8 @@ export const installTranslated = async ({
const { data: rulesToInstall } = await ruleMigrationsClient.data.rules.get(migrationId, {
filters: { ids, installable: true },
from: 0,
size: MAX_TRANSLATED_RULES_TO_INSTALL,
});
const { customRulesToInstall, prebuiltRulesToInstall } = rulesToInstall.reduce(

View file

@ -0,0 +1,84 @@
/*
* 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 { SavedObjectsClientContract } from '@kbn/core/server';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { RuleResponse } from '../../../../../../common/api/detection_engine';
import { createPrebuiltRuleObjectsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client';
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 { convertPrebuiltRuleAssetToRuleResponse } from '../../../../detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response';
import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
export const getUniquePrebuiltRuleIds = (migrationRules: RuleMigration[]): string[] => {
const rulesIds = new Set<string>();
migrationRules.forEach((rule) => {
if (rule.elastic_rule?.prebuilt_rule_id) {
rulesIds.add(rule.elastic_rule.prebuilt_rule_id);
}
});
return Array.from(rulesIds);
};
export interface PrebuiltRulesResults {
/**
* The latest available version
*/
target: RuleResponse;
/**
* The currently installed version
*/
current?: RuleResponse;
}
/**
* Gets Elastic prebuilt rules
* @param rulesClient The rules client to fetch prebuilt rules
* @param savedObjectsClient The saved objects client
* @param rulesIds The list of IDs to filter requested prebuilt rules. If not specified, all available prebuilt rules will be returned.
* @returns
*/
export const getPrebuiltRules = async (
rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract,
rulesIds?: string[]
): Promise<Record<string, PrebuiltRulesResults>> => {
const ruleAssetsClient = createPrebuiltRuleAssetsClient(savedObjectsClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const prebuiltRulesMap = await fetchRuleVersionsTriad({
ruleAssetsClient,
ruleObjectsClient,
});
// Filter out prebuilt rules by `rule_id`
let filteredPrebuiltRulesMap: typeof prebuiltRulesMap;
if (rulesIds) {
filteredPrebuiltRulesMap = new Map();
for (const ruleId of rulesIds) {
const prebuiltRule = prebuiltRulesMap.get(ruleId);
if (prebuiltRule) {
filteredPrebuiltRulesMap.set(ruleId, prebuiltRule);
}
}
} else {
filteredPrebuiltRulesMap = prebuiltRulesMap;
}
const prebuiltRules: Record<string, PrebuiltRulesResults> = {};
filteredPrebuiltRulesMap.forEach((ruleVersions, ruleId) => {
if (ruleVersions.target) {
prebuiltRules[ruleId] = {
target: convertPrebuiltRuleAssetToRuleResponse(ruleVersions.target),
current: ruleVersions.current,
};
}
});
return prebuiltRules;
};

View file

@ -42,6 +42,7 @@ export interface RuleMigrationFilters {
status?: SiemMigrationStatus | SiemMigrationStatus[];
ids?: string[];
installable?: boolean;
prebuilt?: boolean;
searchTerm?: string;
}
export interface RuleMigrationGetOptions {
@ -54,7 +55,6 @@ export interface RuleMigrationGetOptions {
* The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed.
*/
const BULK_MAX_SIZE = 500 as const;
/* The default number of rule migrations to retrieve in a single GET request. */
export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient {
/** Indexes an array of rule migrations to be processed */
@ -337,7 +337,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
private getFilterQuery(
migrationId: string,
{ status, ids, installable, searchTerm }: RuleMigrationFilters = {}
{ status, ids, installable, prebuilt, searchTerm }: RuleMigrationFilters = {}
): QueryDslQueryContainer {
const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }];
if (status) {
@ -353,6 +353,9 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
if (installable) {
filter.push(...conditions.isInstallable());
}
if (prebuilt) {
filter.push(conditions.isPrebuilt());
}
if (searchTerm?.length) {
filter.push(conditions.matchTitle(searchTerm));
}

View file

@ -99,6 +99,7 @@ import {
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationPrebuiltRulesRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
GetRuleMigrationResourcesRequestQueryInput,
GetRuleMigrationResourcesRequestParamsInput,
@ -957,6 +958,27 @@ finalize it.
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query);
},
/**
* Retrieves all available prebuilt rules (installed and installable)
*/
getRuleMigrationPrebuiltRules(
props: GetRuleMigrationPrebuiltRulesProps,
kibanaSpace: string = 'default'
) {
return supertest
.get(
routeWithNamespace(
replaceParams(
'/internal/siem_migrations/rules/{migration_id}/prebuilt_rules',
props.params
),
kibanaSpace
)
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Retrieves resources for an existing SIEM rules migration
*/
@ -1731,6 +1753,9 @@ export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput;
}
export interface GetRuleMigrationPrebuiltRulesProps {
params: GetRuleMigrationPrebuiltRulesRequestParamsInput;
}
export interface GetRuleMigrationResourcesProps {
query: GetRuleMigrationResourcesRequestQueryInput;
params: GetRuleMigrationResourcesRequestParamsInput;