From 70a5bb33c438912b64259ea4c7a3c77c41f93f45 Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Mon, 9 Dec 2024 20:21:16 +0100 Subject: [PATCH] [Rules migration] Add sorting functionality to rules migration table (#11379) (#203396) ## Summary [Internal link](https://github.com/elastic/security-team/issues/10820) to the feature details These changes add sorting functionality to the migration rules table. It is possible to sort migration rules by next columns: `Updated`, `Name`, `Status`, `Risk Score`, `Severity` and `Author`. ### Other changes Next fixes and adjustments were also implemented as part of this PR: * `Installed` status in migration rules table to indicate whether the rule was installed * Rules selection and installation of selected rules * Disable selection for not fully translated rules * `Author` column to show whether the translated rule matched one of the existing Elastic prebuilt rules * `Install and enable` and `Install without enabling` buttons within the migration rule details flyout --- .../kbn-index-adapter/src/field_maps/types.ts | 1 + .../model/api/rules/rule_migration.gen.ts | 10 +- .../api/rules/rule_migration.schema.yaml | 29 +++- .../common/siem_migrations/rules/utils.ts | 6 +- .../public/siem_migrations/rules/api/index.ts | 23 +++- .../components/rule_details_flyout/index.tsx | 3 +- .../components/rules_table/bulk_actions.tsx | 17 ++- .../rules/components/rules_table/index.tsx | 119 ++++++++++++++-- .../components/rules_table/translations.ts | 28 ++++ .../components/rules_table_columns/author.tsx | 39 ++++++ .../components/rules_table_columns/index.tsx | 1 + .../components/rules_table_columns/name.tsx | 11 +- .../rules_table_columns/risk_score.tsx | 2 +- .../rules_table_columns/severity.tsx | 5 +- .../components/rules_table_columns/status.tsx | 4 +- .../rules_table_columns/translations.ts | 21 +++ .../components/status_badge/index.test.tsx | 19 --- .../rules/components/status_badge/index.tsx | 36 +++-- .../components/status_badge/translations.ts | 15 +++ .../use_migration_rules_table_columns.tsx | 2 + .../rules/logic/use_get_migration_rules.ts | 2 + .../logic/use_install_migration_rules.ts | 4 +- .../lib/siem_migrations/rules/api/get.ts | 9 +- .../lib/siem_migrations/rules/api/install.ts | 3 +- .../rules/api/install_translated.ts | 1 + .../rules/api/util/installation.ts | 19 ++- .../data/rule_migrations_data_rules_client.ts | 61 +++------ .../rules/data/rule_migrations_field_maps.ts | 4 +- .../lib/siem_migrations/rules/data/search.ts | 42 ++++++ .../lib/siem_migrations/rules/data/sort.ts | 127 ++++++++++++++++++ 30 files changed, 541 insertions(+), 122 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx delete mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/translations.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts diff --git a/packages/kbn-index-adapter/src/field_maps/types.ts b/packages/kbn-index-adapter/src/field_maps/types.ts index 1cdafc7c61809..90fb44873a342 100644 --- a/packages/kbn-index-adapter/src/field_maps/types.ts +++ b/packages/kbn-index-adapter/src/field_maps/types.ts @@ -46,6 +46,7 @@ export type FieldMap = Record< array?: boolean; doc_values?: boolean; enabled?: boolean; + fields?: Record; format?: string; ignore_above?: number; multi_fields?: MultiField[]; diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 58944ff7f2f95..95a81d4436d8a 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -59,6 +59,8 @@ export type GetRuleMigrationRequestQuery = z.infer; @@ -154,7 +156,13 @@ export type InstallMigrationRulesRequestParamsInput = z.input< >; export type InstallMigrationRulesRequestBody = z.infer; -export const InstallMigrationRulesRequestBody = z.array(NonEmptyString); +export const InstallMigrationRulesRequestBody = z.object({ + ids: z.array(NonEmptyString), + /** + * Indicates whether installed rules should be enabled + */ + enabled: z.boolean().optional(), +}); export type InstallMigrationRulesRequestBodyInput = z.input< typeof InstallMigrationRulesRequestBody >; diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index dff6089b2b48f..b7e495e2ea898 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -133,6 +133,19 @@ paths: required: false schema: type: number + - name: sort_field + in: query + required: false + schema: + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + - name: sort_direction + in: query + required: false + schema: + type: string + enum: + - asc + - desc - name: search_term in: query required: false @@ -180,10 +193,18 @@ paths: content: application/json: schema: - type: array - items: - description: The rule migration id - $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + type: object + required: + - ids + properties: + ids: + type: array + items: + description: The rule migration id + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + enabled: + type: boolean + description: Indicates whether installed rules should be enabled responses: 200: description: Indicates rules migrations have been installed correctly. diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/utils.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/utils.ts index a9b8666b19981..8763e057052b5 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/rules/utils.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/rules/utils.ts @@ -22,13 +22,17 @@ export const isMigrationCustomRule = (rule?: ElasticRule): rule is MigrationCust !isMigrationPrebuiltRule(rule) && !!(rule?.title && rule?.description && rule?.query && rule?.query_language); -export const convertMigrationCustomRuleToSecurityRulePayload = (rule: MigrationCustomRule) => { +export const convertMigrationCustomRuleToSecurityRulePayload = ( + rule: MigrationCustomRule, + enabled: boolean +) => { return { type: rule.query_language, language: rule.query_language, query: rule.query, name: rule.title, description: rule.description, + enabled, ...DEFAULT_TRANSLATION_FIELDS, severity: (rule.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY, diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts index ac9e49ff861fc..57fb5d0422093 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -120,6 +120,10 @@ export interface GetRuleMigrationParams { page?: number; /** Optional number of documents per page to retrieve */ perPage?: number; + /** Optional field of the rule migration object to sort results by */ + sortField?: string; + /** Optional direction to sort results by */ + sortDirection?: 'asc' | 'desc'; /** Optional search term to filter documents */ searchTerm?: string; /** Optional AbortSignal for cancelling request */ @@ -130,12 +134,24 @@ export const getRuleMigrations = async ({ migrationId, page, perPage, + sortField, + sortDirection, searchTerm, signal, }: GetRuleMigrationParams): Promise => { return KibanaServices.get().http.get( replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), - { version: '1', query: { page, per_page: perPage, search_term: searchTerm }, signal } + { + version: '1', + query: { + page, + per_page: perPage, + sort_field: sortField, + sort_direction: sortDirection, + search_term: searchTerm, + }, + signal, + } ); }; @@ -163,6 +179,8 @@ export interface InstallRulesParams { migrationId: string; /** The rule ids to install */ ids: string[]; + /** Optional indicator to enable the installed rule */ + enabled?: boolean; /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; } @@ -170,11 +188,12 @@ export interface InstallRulesParams { export const installMigrationRules = async ({ migrationId, ids, + enabled, signal, }: InstallRulesParams): Promise => { return KibanaServices.get().http.post( replaceParams(SIEM_RULE_MIGRATION_INSTALL_PATH, { migration_id: migrationId }), - { version: '1', body: JSON.stringify(ids), signal } + { version: '1', body: JSON.stringify({ ids, enabled }), signal } ); }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx index 8fea17b51cb0e..9762cc578e0cc 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx @@ -84,7 +84,8 @@ export const MigrationRuleDetailsFlyout: React.FC { if (isMigrationCustomRule(ruleMigration.elastic_rule)) { return convertMigrationCustomRuleToSecurityRulePayload( - ruleMigration.elastic_rule + ruleMigration.elastic_rule, + false ) as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter; } return matchedPrebuiltRule; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx index a58681b6e771f..8f32308ed52c4 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx @@ -6,7 +6,13 @@ */ import React from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, +} from '@elastic/eui'; import * as i18n from './translations'; export interface BulkActionsProps { @@ -29,13 +35,14 @@ export const BulkActions: React.FC = React.memo( installSelectedRule, }) => { const disableInstallTranslatedRulesButton = isTableLoading || !numberOfTranslatedRules; - const showInstallSelectedRulesButton = - disableInstallTranslatedRulesButton && numberOfSelectedRules > 0; + const showInstallSelectedRulesButton = isTableLoading || numberOfSelectedRules > 0; return ( {showInstallSelectedRulesButton ? ( - = React.memo( > {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} {isTableLoading && } - + ) : null} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx index 13b451c2918d8..106e7ba514d3f 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { CriteriaWithPagination } from '@elastic/eui'; +import type { CriteriaWithPagination, EuiTableSelectionType } from '@elastic/eui'; import { EuiSkeletonLoading, EuiSkeletonTitle, @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiSpacer, EuiBasicTable, + EuiButton, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; @@ -30,8 +31,12 @@ import { useGetMigrationPrebuiltRules } from '../../logic/use_get_migration_preb import * as logicI18n from '../../logic/translations'; import { BulkActions } from './bulk_actions'; import { SearchField } from './search_field'; +import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants'; +import * as i18n from './translations'; const DEFAULT_PAGE_SIZE = 10; +const DEFAULT_SORT_FIELD = 'translation_result'; +const DEFAULT_SORT_DIRECTION = 'desc'; export interface MigrationRulesTableProps { /** @@ -49,6 +54,8 @@ export const MigrationRulesTable: React.FC = React.mem const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); const [searchTerm, setSearchTerm] = useState(); const { data: translationStats, isLoading: isStatsLoading } = @@ -64,10 +71,33 @@ export const MigrationRulesTable: React.FC = React.mem migrationId, page: pageIndex, perPage: pageSize, + sortField, + sortDirection, searchTerm, }); const [selectedRuleMigrations, setSelectedRuleMigrations] = useState([]); + const tableSelection: EuiTableSelectionType = useMemo( + () => ({ + selectable: (item: RuleMigration) => { + return ( + !item.elastic_rule?.id && + item.translation_result === SiemMigrationRuleTranslationResult.FULL + ); + }, + selectableMessage: (selectable: boolean, item: RuleMigration) => { + if (selectable) { + return ''; + } + return item.elastic_rule?.id + ? i18n.ALREADY_TRANSLATED_RULE_TOOLTIP + : i18n.NOT_FULLY_TRANSLATED_RULE_TOOLTIP; + }, + onSelectionChange: setSelectedRuleMigrations, + selected: selectedRuleMigrations, + }), + [selectedRuleMigrations] + ); const pagination = useMemo(() => { return { @@ -77,11 +107,25 @@ export const MigrationRulesTable: React.FC = React.mem }; }, [pageIndex, pageSize, total]); + const sorting = useMemo(() => { + return { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + }, [sortDirection, sortField]); + const onTableChange = useCallback(({ page, sort }: CriteriaWithPagination) => { if (page) { setPageIndex(page.index); setPageSize(page.size); } + if (sort) { + const { field, direction } = sort; + setSortField(field); + setSortDirection(direction); + } }, []); const handleOnSearch = useCallback((value: string) => { @@ -94,10 +138,10 @@ export const MigrationRulesTable: React.FC = React.mem const [isTableLoading, setTableLoading] = useState(false); const installSingleRule = useCallback( - async (migrationRule: RuleMigration, enable?: boolean) => { + async (migrationRule: RuleMigration, enabled = false) => { setTableLoading(true); try { - await installMigrationRules([migrationRule.id]); + await installMigrationRules({ ids: [migrationRule.id], enabled }); } catch (error) { addError(error, { title: logicI18n.INSTALL_MIGRATION_RULES_FAILURE }); } finally { @@ -107,6 +151,24 @@ export const MigrationRulesTable: React.FC = React.mem [addError, installMigrationRules] ); + const installSelectedRule = useCallback( + async (enabled = false) => { + setTableLoading(true); + try { + await installMigrationRules({ + ids: selectedRuleMigrations.map((rule) => rule.id), + enabled, + }); + } catch (error) { + addError(error, { title: logicI18n.INSTALL_MIGRATION_RULES_FAILURE }); + } finally { + setTableLoading(false); + setSelectedRuleMigrations([]); + } + }, + [addError, installMigrationRules, selectedRuleMigrations] + ); + const installTranslatedRules = useCallback( async (enable?: boolean) => { setTableLoading(true); @@ -121,12 +183,45 @@ export const MigrationRulesTable: React.FC = React.mem [addError, installTranslatedMigrationRules] ); + const isLoading = isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading; + const ruleActionsFactory = useCallback( (ruleMigration: RuleMigration, closeRulePreview: () => void) => { - // TODO: Add flyout action buttons - return null; + const canMigrationRuleBeInstalled = + !isLoading && + !ruleMigration.elastic_rule?.id && + ruleMigration.translation_result === SiemMigrationRuleTranslationResult.FULL; + return ( + + + { + installSingleRule(ruleMigration); + closeRulePreview(); + }} + data-test-subj="installMigrationRuleFromFlyoutButton" + > + {i18n.INSTALL_WITHOUT_ENABLING_BUTTON_LABEL} + + + + { + installSingleRule(ruleMigration, true); + closeRulePreview(); + }} + fill + data-test-subj="installAndEnableMigrationRuleFromFlyoutButton" + > + {i18n.INSTALL_AND_ENABLE_BUTTON_LABEL} + + + + ); }, - [] + [installSingleRule, isLoading] ); const { @@ -143,8 +238,6 @@ export const MigrationRulesTable: React.FC = React.mem installMigrationRule: installSingleRule, }); - const isLoading = isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading; - return ( <> = React.mem @@ -178,12 +272,9 @@ export const MigrationRulesTable: React.FC = React.mem loading={isTableLoading} items={ruleMigrations} pagination={pagination} + sorting={sorting} onChange={onTableChange} - selection={{ - selectable: () => true, - onSelectionChange: setSelectedRuleMigrations, - initialSelected: selectedRuleMigrations, - }} + selection={tableSelection} itemId={'id'} data-test-subj={'rules-translation-table'} columns={rulesColumns} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts index 6803deb895d9b..79b5a1fe00900 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts @@ -80,3 +80,31 @@ export const INSTALL_TRANSLATED_ARIA_LABEL = i18n.translate( defaultMessage: 'Install all translated rules', } ); + +export const ALREADY_TRANSLATED_RULE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.alreadyTranslatedTooltip', + { + defaultMessage: 'Already translated migration rule', + } +); + +export const NOT_FULLY_TRANSLATED_RULE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.notFullyTranslatedTooltip', + { + defaultMessage: 'Not fully translated migration rule', + } +); + +export const INSTALL_WITHOUT_ENABLING_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.installWithoutEnablingButtonLabel', + { + defaultMessage: 'Install without enabling', + } +); + +export const INSTALL_AND_ENABLE_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.installAndEnableButtonLabel', + { + defaultMessage: 'Install and enable', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx new file mode 100644 index 0000000000000..23980f5612f89 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx @@ -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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; +import type { TableColumn } from './constants'; + +const Author = ({ isPrebuiltRule }: { isPrebuiltRule: boolean }) => { + return ( + + {isPrebuiltRule && ( + + + + )} + + {isPrebuiltRule ? i18n.ELASTIC_AUTHOR_TITLE : i18n.CUSTOM_AUTHOR_TITLE} + + + ); +}; + +export const createAuthorColumn = (): TableColumn => { + return { + field: 'elastic_rule.prebuilt_rule_id', + name: i18n.COLUMN_AUTHOR, + render: (_, rule: RuleMigration) => { + return ; + }, + sortable: true, + width: '10%', + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/index.tsx index 4220a35ed4622..61af73ca7b9f8 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/index.tsx @@ -8,6 +8,7 @@ export * from './constants'; export * from './actions'; +export * from './author'; export * from './name'; export * from './risk_score'; export * from './severity'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx index 085a2f5c6a254..dd77636521eda 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx @@ -12,12 +12,11 @@ import * as i18n from './translations'; import type { TableColumn } from './constants'; interface NameProps { - name: string; rule: RuleMigration; openMigrationRuleDetails: (rule: RuleMigration) => void; } -const Name = ({ name, rule, openMigrationRuleDetails }: NameProps) => { +const Name = ({ rule, openMigrationRuleDetails }: NameProps) => { return ( { @@ -25,7 +24,7 @@ const Name = ({ name, rule, openMigrationRuleDetails }: NameProps) => { }} data-test-subj="ruleName" > - {name} + {rule.elastic_rule?.title} ); }; @@ -36,10 +35,10 @@ export const createNameColumn = ({ openMigrationRuleDetails: (rule: RuleMigration) => void; }): TableColumn => { return { - field: 'original_rule.title', + field: 'elastic_rule.title', name: i18n.COLUMN_NAME, - render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => ( - + render: (_, rule: RuleMigration) => ( + ), sortable: true, truncateText: true, diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx index e9eca65736a51..0fb78ae8bf709 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx @@ -22,6 +22,6 @@ export const createRiskScoreColumn = (): TableColumn => { ), sortable: true, truncateText: true, - width: '75px', + width: '10%', }; }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx index 4ea737844ad53..9a6c0b98ff317 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx @@ -8,9 +8,7 @@ import React from 'react'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { DEFAULT_TRANSLATION_SEVERITY } from '../../../../../common/siem_migrations/constants'; -import { getNormalizedSeverity } from '../../../../detection_engine/rule_management_ui/components/rules_table/helpers'; import { SeverityBadge } from '../../../../common/components/severity_badge'; -import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import type { TableColumn } from './constants'; import * as i18n from './translations'; @@ -19,8 +17,7 @@ export const createSeverityColumn = (): TableColumn => { field: 'elastic_rule.severity', name: i18n.COLUMN_SEVERITY, render: (value?: Severity) => , - sortable: ({ elastic_rule: elasticRule }: RuleMigration) => - getNormalizedSeverity((elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY), + sortable: true, truncateText: true, width: '12%', }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx index 982f6ba7580b2..5daec1f1b4fa9 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx @@ -18,8 +18,8 @@ export const createStatusColumn = (): TableColumn => { render: (value: RuleMigration['translation_result'], rule: RuleMigration) => ( ), - sortable: false, + sortable: true, truncateText: true, - width: '12%', + width: '15%', }; }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts index 5b40abd3d7485..64e459a609143 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts @@ -14,6 +14,27 @@ export const COLUMN_ACTIONS = i18n.translate( } ); +export const COLUMN_AUTHOR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.authorLabel', + { + defaultMessage: 'Author', + } +); + +export const ELASTIC_AUTHOR_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.elasticAuthorTitle', + { + defaultMessage: 'Elastic', + } +); + +export const CUSTOM_AUTHOR_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.customAuthorTitle', + { + defaultMessage: 'Custom', + } +); + export const ACTIONS_VIEW_LABEL = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsViewLabel', { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.test.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.test.tsx deleted file mode 100644 index aaf256cfb60b5..0000000000000 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { StatusBadge } from '.'; - -describe('StatusBadge', () => { - it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('HealthTruncateText')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx index 60f0ed94862ca..8f8bcff40f674 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx @@ -8,9 +8,16 @@ import React from 'react'; import { euiLightVars } from '@kbn/ui-theme'; +import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { css } from '@emotion/css'; import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; import { convertTranslationResultIntoText } from '../../utils/helpers'; +import * as i18n from './translations'; + +const statusTextWrapperClassName = css` + width: 100%; + display: inline-grid; +`; const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars; const statusToColorMap: Record = { @@ -28,17 +35,28 @@ interface StatusBadgeProps { export const StatusBadge: React.FC = React.memo( ({ value, installedRuleId, 'data-test-subj': dataTestSubj = 'translation-result' }) => { const translationResult = installedRuleId ? 'full' : value ?? 'untranslatable'; - const displayValue = convertTranslationResultIntoText(translationResult); + const displayValue = installedRuleId + ? i18n.RULE_STATUS_INSTALLED + : convertTranslationResultIntoText(translationResult); const color = statusToColorMap[translationResult]; return ( - - {displayValue} - + + {installedRuleId ? ( + + + + + {displayValue} + + ) : ( + +
+ {displayValue} +
+
+ )} +
); } ); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/translations.ts new file mode 100644 index 0000000000000..0a7b1c37f7acf --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULE_STATUS_INSTALLED = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.status.installedLabel', + { + defaultMessage: 'Installed', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rules_table_columns.tsx index b8b37bccaffd3..3c2d5a696ccc1 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rules_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rules_table_columns.tsx @@ -10,6 +10,7 @@ import type { RuleMigration } from '../../../../common/siem_migrations/model/rul import type { TableColumn } from '../components/rules_table_columns'; import { createActionsColumn, + createAuthorColumn, createNameColumn, createRiskScoreColumn, createSeverityColumn, @@ -33,6 +34,7 @@ export const useMigrationRulesTableColumns = ({ createStatusColumn(), createRiskScoreColumn(), createSeverityColumn(), + createAuthorColumn(), createActionsColumn({ disableActions, openMigrationRuleDetails, diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts index 5f59ceb9f76c2..4109575459233 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts @@ -18,6 +18,8 @@ export const useGetMigrationRules = (params: { migrationId: string; page?: number; perPage?: number; + sortField: string; + sortDirection: 'asc' | 'desc'; searchTerm?: string; }) => { const { addError } = useAppToasts(); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts index 755faa03bff14..2b28b3b944990 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts @@ -23,8 +23,8 @@ export const useInstallMigrationRules = (migrationId: string) => { const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats(migrationId); - return useMutation( - (ids: string[]) => installMigrationRules({ migrationId, ids }), + return useMutation( + ({ ids, enabled = false }) => installMigrationRules({ migrationId, ids, enabled }), { mutationKey: INSTALL_MIGRATION_RULES_MUTATION_KEY, onError: (error) => { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts index dd13a75cdf83a..30037aeea88ae 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts @@ -39,13 +39,20 @@ export const registerSiemRuleMigrationsGetRoute = ( }, withLicense(async (context, req, res): Promise> => { const { migration_id: migrationId } = req.params; - const { page, per_page: perPage, search_term: searchTerm } = req.query; + const { + page, + per_page: perPage, + sort_field: sortField, + sort_direction: sortDirection, + search_term: searchTerm, + } = req.query; try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); const options: RuleMigrationGetOptions = { filters: { searchTerm }, + sort: { sortField, sortDirection }, size: perPage, from: page && perPage ? page * perPage : 0, }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts index 7b41ea536aadf..9fae2922b486f 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts @@ -40,7 +40,7 @@ export const registerSiemRuleMigrationsInstallRoute = ( withLicense( async (context, req, res): Promise> => { const { migration_id: migrationId } = req.params; - const ids = req.body; + const { ids, enabled = false } = req.body; try { const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); @@ -52,6 +52,7 @@ export const registerSiemRuleMigrationsInstallRoute = ( await installTranslated({ migrationId, ids, + enabled, securitySolutionContext, savedObjectsClient, rulesClient, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/install_translated.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/install_translated.ts index ac6a598c4b92f..2cf2a2e2c8efd 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/install_translated.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/install_translated.ts @@ -50,6 +50,7 @@ export const registerSiemRuleMigrationsInstallTranslatedRoute = ( await installTranslated({ migrationId, + enabled: false, securitySolutionContext, savedObjectsClient, rulesClient, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts index d74619e4c1251..8716c83ce6ba3 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts @@ -27,6 +27,7 @@ import { const installPrebuiltRules = async ( rulesToInstall: StoredRuleMigration[], + enabled: boolean, securitySolutionContext: SecuritySolutionApiRequestHandlerContext, rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, @@ -41,7 +42,7 @@ const installPrebuiltRules = async ( if (item.current) { acc.installed.push(item.current); } else { - acc.installable.push(item.target); + acc.installable.push({ ...item.target, enabled }); } return acc; }, @@ -85,6 +86,7 @@ const installPrebuiltRules = async ( export const installCustomRules = async ( rulesToInstall: StoredRuleMigration[], + enabled: boolean, detectionRulesClient: IDetectionRulesClient, logger: Logger ): Promise => { @@ -96,8 +98,11 @@ export const installCustomRules = async ( if (!isMigrationCustomRule(rule.elastic_rule)) { return; } - const payloadRule = convertMigrationCustomRuleToSecurityRulePayload(rule.elastic_rule); - const createdRule = await detectionRulesClient.createPrebuiltRule({ + const payloadRule = convertMigrationCustomRuleToSecurityRulePayload( + rule.elastic_rule, + enabled + ); + const createdRule = await detectionRulesClient.createCustomRule({ params: payloadRule, }); rulesToUpdate.push({ @@ -131,6 +136,11 @@ interface InstallTranslatedProps { */ ids?: string[]; + /** + * Indicates whether the installed migration rules should be enabled + */ + enabled: boolean; + /** * The security solution context */ @@ -155,6 +165,7 @@ interface InstallTranslatedProps { export const installTranslated = async ({ migrationId, ids, + enabled, securitySolutionContext, rulesClient, savedObjectsClient, @@ -186,6 +197,7 @@ export const installTranslated = async ({ const updatedPrebuiltRules = await installPrebuiltRules( prebuiltRulesToInstall, + enabled, securitySolutionContext, rulesClient, savedObjectsClient, @@ -194,6 +206,7 @@ export const installTranslated = async ({ const updatedCustomRules = await installCustomRules( customRulesToInstall, + enabled, detectionRulesClient, logger ); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index f11b24e50b95a..1eeb3ced0572a 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -15,10 +15,7 @@ import type { QueryDslQueryContainer, } from '@elastic/elasticsearch/lib/api/types'; import type { StoredRuleMigration } from '../types'; -import { - SiemMigrationRuleTranslationResult, - SiemMigrationStatus, -} from '../../../../../common/siem_migrations/constants'; +import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; import type { ElasticRule, RuleMigration, @@ -26,6 +23,8 @@ import type { RuleMigrationTranslationStats, } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; +import { getSortingOptions, type RuleMigrationSort } from './sort'; +import { conditions as searchConditions } from './search'; export type CreateRuleMigrationInput = Omit< RuleMigration, @@ -47,6 +46,7 @@ export interface RuleMigrationFilters { } export interface RuleMigrationGetOptions { filters?: RuleMigrationFilters; + sort?: RuleMigrationSort; from?: number; size?: number; } @@ -120,13 +120,19 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves an array of rule documents of a specific migrations */ async get( migrationId: string, - { filters = {}, from, size }: RuleMigrationGetOptions = {} + { filters = {}, sort = {}, from, size }: RuleMigrationGetOptions = {} ): Promise<{ total: number; data: StoredRuleMigration[] }> { const index = await this.getIndexName(); const query = this.getFilterQuery(migrationId, { ...filters }); const result = await this.esClient - .search({ index, query, sort: '_doc', from, size }) + .search({ + index, + query, + sort: sort.sortField ? getSortingOptions(sort) : undefined, + from, + size, + }) .catch((error) => { this.logger.error(`Error searching rule migrations: ${error.message}`); throw error; @@ -238,8 +244,8 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient const query = this.getFilterQuery(migrationId); const aggregations = { - prebuilt: { filter: conditions.isPrebuilt() }, - installable: { filter: { bool: { must: conditions.isInstallable() } } }, + prebuilt: { filter: searchConditions.isPrebuilt() }, + installable: { filter: { bool: { must: searchConditions.isInstallable() } } }, }; const result = await this.esClient .search({ index, query, aggregations, _source: false }) @@ -351,47 +357,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient filter.push({ terms: { _id: ids } }); } if (installable) { - filter.push(...conditions.isInstallable()); + filter.push(...searchConditions.isInstallable()); } if (prebuilt) { - filter.push(conditions.isPrebuilt()); + filter.push(searchConditions.isPrebuilt()); } if (searchTerm?.length) { - filter.push(conditions.matchTitle(searchTerm)); + filter.push(searchConditions.matchTitle(searchTerm)); } return { bool: { filter } }; } } - -const conditions = { - isFullyTranslated(): QueryDslQueryContainer { - return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } }; - }, - isNotInstalled(): QueryDslQueryContainer { - return { - nested: { - path: 'elastic_rule', - query: { bool: { must_not: { exists: { field: 'elastic_rule.id' } } } }, - }, - }; - }, - isPrebuilt(): QueryDslQueryContainer { - return { - nested: { - path: 'elastic_rule', - query: { exists: { field: 'elastic_rule.prebuilt_rule_id' } }, - }, - }; - }, - matchTitle(title: string): QueryDslQueryContainer { - return { - nested: { - path: 'elastic_rule', - query: { match: { 'elastic_rule.title': title } }, - }, - }; - }, - isInstallable(): QueryDslQueryContainer[] { - return [this.isFullyTranslated(), this.isNotInstalled()]; - }, -}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index 7aca804c12890..952663c36123c 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -19,14 +19,14 @@ export const ruleMigrationsFieldMap: FieldMap + direction === 'desc' ? '_last' : '_first'; + +const sortingOptions = { + matchedPrebuiltRule(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + return [ + { + 'elastic_rule.prebuilt_rule_id': { + order: direction, + nested: { path: 'elastic_rule' }, + missing: sortMissingValue(direction), + }, + }, + ]; + }, + severity(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + const field = 'elastic_rule.severity'; + return [ + { + _script: { + order: direction, + type: 'number', + script: { + source: ` + if (doc.containsKey('${field}') && !doc['${field}'].empty) { + def value = doc['${field}'].value.toLowerCase(); + if (value == 'critical') { return 3 } + if (value == 'high') { return 2 } + if (value == 'medium') { return 1 } + if (value == 'low') { return 0 } + } + return -1; + `, + lang: 'painless', + }, + nested: { path: 'elastic_rule' }, + }, + }, + ]; + }, + status(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + const field = 'translation_result'; + const installedRuleField = 'elastic_rule.id'; + return [ + { + _script: { + order: direction, + type: 'number', + script: { + source: ` + if (doc.containsKey('${field}') && !doc['${field}'].empty) { + def value = doc['${field}'].value.toLowerCase(); + if (value == 'full') { return 2 } + if (value == 'partial') { return 1 } + if (value == 'untranslatable') { return 0 } + } + return -1; + `, + lang: 'painless', + }, + }, + }, + { + _script: { + order: direction, + type: 'number', + script: { + source: ` + if (doc.containsKey('${installedRuleField}') && !doc['${installedRuleField}'].empty) { + return 0; + } + return -1; + `, + lang: 'painless', + }, + nested: { path: 'elastic_rule' }, + }, + }, + ]; + }, + updated(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + return [{ updated_at: direction }]; + }, + name(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + return [ + { 'elastic_rule.title.keyword': { order: direction, nested: { path: 'elastic_rule' } } }, + ]; + }, +}; + +const DEFAULT_SORTING: estypes.Sort = [ + ...sortingOptions.status('desc'), + ...sortingOptions.matchedPrebuiltRule('desc'), + ...sortingOptions.severity(), + ...sortingOptions.updated(), +]; + +const sortingOptionsMap: { + [key: string]: (direction?: estypes.SortOrder) => estypes.SortCombinations[]; +} = { + 'elastic_rule.title': sortingOptions.name, + 'elastic_rule.severity': sortingOptions.severity, + 'elastic_rule.prebuilt_rule_id': sortingOptions.matchedPrebuiltRule, + translation_result: sortingOptions.status, + updated_at: sortingOptions.updated, +}; + +export const getSortingOptions = (sort?: RuleMigrationSort): estypes.Sort => { + if (!sort?.sortField) { + return DEFAULT_SORTING; + } + return sortingOptionsMap[sort.sortField]?.(sort.sortDirection) ?? DEFAULT_SORTING; +};