Skip to content

Commit

Permalink
[Rules migration] Add functionality to display matched prebuilt rules…
Browse files Browse the repository at this point in the history
… details (elastic#11360) (elastic#203035)

## Summary

[Internal link](elastic/security-team#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 <[email protected]>
Co-authored-by: Sergi Massaneda <[email protected]>
  • Loading branch information
3 people authored Dec 8, 2024
1 parent 194a324 commit b3d6d91
Show file tree
Hide file tree
Showing 22 changed files with 638 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ import type {
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
GetRuleMigrationResponse,
GetRuleMigrationPrebuiltRulesRequestParamsInput,
GetRuleMigrationPrebuiltRulesResponse,
GetRuleMigrationResourcesRequestQueryInput,
GetRuleMigrationResourcesRequestParamsInput,
GetRuleMigrationResourcesResponse,
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -2396,6 +2416,9 @@ export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput;
}
export interface GetRuleMigrationPrebuiltRulesProps {
params: GetRuleMigrationPrebuiltRulesRequestParamsInput;
}
export interface GetRuleMigrationResourcesProps {
query: GetRuleMigrationResourcesRequestQueryInput;
params: GetRuleMigrationResourcesRequestParamsInput;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
OriginalRule,
RuleMigration,
RuleMigrationTranslationStats,
PrebuiltRuleVersion,
RuleMigrationResourceData,
RuleMigrationResourceType,
RuleMigrationResource,
Expand Down Expand Up @@ -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
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,6 +31,7 @@ import type {
InstallMigrationRulesResponse,
StartRuleMigrationRequestBody,
GetRuleMigrationStatsResponse,
GetRuleMigrationPrebuiltRulesResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';

export interface GetRuleMigrationStatsParams {
Expand Down Expand Up @@ -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 }
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,23 @@ 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 {
DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS,
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.
Expand All @@ -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;
Expand All @@ -76,38 +74,36 @@ 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(
() => ({
id: 'translation',
name: i18n.TRANSLATION_TAB_LABEL,
content: (
<TabContentPadding>
<TranslationTab ruleMigration={ruleMigration} />
<TranslationTab
ruleMigration={ruleMigration}
matchedPrebuiltRule={matchedPrebuiltRule}
/>
</TabContentPadding>
),
}),
[ruleMigration]
[matchedPrebuiltRule, ruleMigration]
);

const overviewTab: EuiTabbedContentTab = useMemo(
Expand All @@ -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>
),
}),
Expand Down Expand Up @@ -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>
Expand Down
Loading

0 comments on commit b3d6d91

Please sign in to comment.