mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
## Summary [Internal link](https://github.com/elastic/security-team/issues/10820) to the feature details These changes add 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:
parent
194a324b75
commit
b3d6d914b3
22 changed files with 638 additions and 200 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
>;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 }
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue