diff --git a/packages/deeplinks/security/deep_links.ts b/packages/deeplinks/security/deep_links.ts index 644691bd5b8bc..c1d9b3b3cb6af 100644 --- a/packages/deeplinks/security/deep_links.ts +++ b/packages/deeplinks/security/deep_links.ts @@ -69,6 +69,7 @@ export enum SecurityPageName { rulesAdd = 'rules-add', rulesCreate = 'rules-create', rulesLanding = 'rules-landing', + siemMigrationsRules = 'siem_migrations-rules', /* * Warning: Computed values are not permitted in an enum with string valued members * All threat intelligence page names must match `TIPageId` in x-pack/plugins/threat_intelligence/public/common/navigation/types.ts diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index b366a0e555357..265af5a47e1fe 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -138,6 +138,8 @@ export const APP_BLOCKLIST_PATH = `${APP_PATH}${BLOCKLIST_PATH}` as const; export const APP_RESPONSE_ACTIONS_HISTORY_PATH = `${APP_PATH}${RESPONSE_ACTIONS_HISTORY_PATH}` as const; export const NOTES_PATH = `${MANAGEMENT_PATH}/notes` as const; +export const SIEM_MIGRATIONS_PATH = '/siem_migrations' as const; +export const SIEM_MIGRATIONS_RULES_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const; // cloud logs to exclude from default index pattern export const EXCLUDE_ELASTIC_CLOUD_INDICES = ['-*elastic-cloud-logs-*']; diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 709bb5f614f7b..1769a805f488f 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -101,6 +101,13 @@ export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exce defaultMessage: 'Shared exception lists', }); +export const SIEM_MIGRATIONS_RULES = i18n.translate( + 'xpack.securitySolution.navigation.siemMigrationsRules', + { + defaultMessage: 'SIEM Rules Migrations', + } +); + export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', { defaultMessage: 'Alerts', }); diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx index 8bbba3885a2ab..1be423c988397 100644 --- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -29,6 +29,7 @@ import { EntityAnalytics } from './entity_analytics'; import { Assets } from './assets'; import { Investigations } from './investigations'; import { MachineLearning } from './machine_learning'; +import { SiemMigrations } from './siem_migrations'; /** * The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import. @@ -53,5 +54,6 @@ const subPluginClasses = { Assets, Investigations, MachineLearning, + SiemMigrations, }; export { subPluginClasses }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index b74d0cffdc88d..f933832264247 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -245,6 +245,7 @@ export class Plugin implements IPlugin/x-pack/plugins/security_solution/public/siem_migrations'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/siem_migrations', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/siem_migrations/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../../server/__mocks__/module_name_map'), +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/links.ts b/x-pack/plugins/security_solution/public/siem_migrations/links.ts new file mode 100644 index 0000000000000..34db8a357785a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/links.ts @@ -0,0 +1,35 @@ +/* + * 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'; +import { + SecurityPageName, + SERVER_APP_ID, + SIEM_MIGRATIONS_RULES_PATH, +} from '../../common/constants'; +import { SIEM_MIGRATIONS_RULES } from '../app/translations'; +import type { LinkItem } from '../common/links/types'; +import { IconConsoleCloud } from '../common/icons/console_cloud'; + +export const siemMigrationsLinks: LinkItem = { + id: SecurityPageName.siemMigrationsRules, + title: SIEM_MIGRATIONS_RULES, + description: i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRulesDescription', { + defaultMessage: 'SIEM Rules Migrations.', + }), + landingIcon: IconConsoleCloud, + path: SIEM_MIGRATIONS_RULES_PATH, + capabilities: [`${SERVER_APP_ID}.show`], + skipUrlState: true, + hideTimeline: true, + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRules', { + defaultMessage: 'SIEM Rules Migrations', + }), + ], + experimentalKey: 'siemMigrationsEnabled', +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/routes.tsx b/x-pack/plugins/security_solution/public/siem_migrations/routes.tsx new file mode 100644 index 0000000000000..610eb7e2a72d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/routes.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import type { SecuritySubPluginRoutes } from '../app/types'; +import { SIEM_MIGRATIONS_RULES_PATH, SecurityPageName } from '../../common/constants'; +import { RulesPage } from './rules/pages'; +import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; +import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper'; + +export const RulesRoutes = () => { + return ( + + + + + + ); +}; + +export const routes: SecuritySubPluginRoutes = [ + { + path: SIEM_MIGRATIONS_RULES_PATH, + component: RulesRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts new file mode 100644 index 0000000000000..7232cb722bd1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -0,0 +1,66 @@ +/* + * 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 { KibanaServices } from '../../../common/lib/kibana'; + +import { + SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, + SIEM_RULE_MIGRATION_PATH, +} from '../../../../common/siem_migrations/constants'; +import type { + GetAllStatsRuleMigrationResponse, + GetRuleMigrationResponse, +} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; + +/** + * Retrieves the stats for all the existing migrations, aggregated by `migration_id`. + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRuleMigrationsStatsAll = async ({ + signal, +}: { + signal: AbortSignal | undefined; +}): Promise => { + return KibanaServices.get().http.fetch( + SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, + { + method: 'GET', + version: '1', + signal, + } + ); +}; + +/** + * Retrieves all the migration rule documents of a specific migration. + * + * @param migrationId `id` of the migration to retrieve rule documents for + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRuleMigrations = async ({ + migrationId, + signal, +}: { + migrationId: string; + signal: AbortSignal | undefined; +}): Promise => { + return KibanaServices.get().http.fetch( + replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), + { + method: 'GET', + version: '1', + signal, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/constants.ts new file mode 100644 index 0000000000000..61e0d1e05f7f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/constants.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +const ONE_MINUTE = 60000; + +export const DEFAULT_QUERY_OPTIONS = { + refetchIntervalInBackground: false, + staleTime: ONE_MINUTE * 5, +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.ts new file mode 100644 index 0000000000000..76cf01c6c35d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.ts @@ -0,0 +1,33 @@ +/* + * 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 { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { replaceParams } from '@kbn/openapi-common/shared'; +import { DEFAULT_QUERY_OPTIONS } from './constants'; +import { getRuleMigrations } from '../api'; +import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; + +export const useGetRuleMigrationsQuery = ( + migrationId: string, + options?: UseQueryOptions +) => { + const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, { + migration_id: migrationId, + }); + return useQuery( + ['GET', SPECIFIC_MIGRATION_PATH], + async ({ signal }) => { + return getRuleMigrations({ migrationId, signal }); + }, + { + ...DEFAULT_QUERY_OPTIONS, + ...options, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations_stats_all.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations_stats_all.ts new file mode 100644 index 0000000000000..026e407050e97 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations_stats_all.ts @@ -0,0 +1,30 @@ +/* + * 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 { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { DEFAULT_QUERY_OPTIONS } from './constants'; +import { getRuleMigrationsStatsAll } from '../api'; +import type { GetAllStatsRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATIONS_ALL_STATS_PATH } from '../../../../../common/siem_migrations/constants'; + +export const GET_RULE_MIGRATIONS_STATS_ALL_QUERY_KEY = ['GET', SIEM_RULE_MIGRATIONS_ALL_STATS_PATH]; + +export const useGetRuleMigrationsStatsAllQuery = ( + options?: UseQueryOptions +) => { + return useQuery( + GET_RULE_MIGRATIONS_STATS_ALL_QUERY_KEY, + async ({ signal }) => { + return getRuleMigrationsStatsAll({ signal }); + }, + { + ...DEFAULT_QUERY_OPTIONS, + ...options, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx new file mode 100644 index 0000000000000..ba73bd9c84946 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx @@ -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 React, { useMemo } from 'react'; + +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +export interface HeaderButtonsProps { + /** + * Available rule migrations ids + */ + migrationsIds: string[]; + + /** + * Selected rule migration id + */ + selectedMigrationId: string | undefined; + + /** + * Handles migration selection changes + * @param selectedId Selected migration id + * @returns + */ + onMigrationIdChange: (selectedId?: string) => void; +} + +const HeaderButtonsComponent: React.FC = ({ + migrationsIds, + selectedMigrationId, + onMigrationIdChange, +}) => { + const migrationOptions = useMemo(() => { + const options: Array> = migrationsIds.map((id, index) => ({ + value: id, + 'data-test-subj': `migrationSelectionOption-${index}`, + label: i18n.SIEM_MIGRATIONS_OPTION_LABEL(index + 1), + })); + return options; + }, [migrationsIds]); + const selectedMigrationOption = useMemo>>(() => { + const index = migrationsIds.findIndex((id) => id === selectedMigrationId); + return index !== -1 + ? [ + { + value: selectedMigrationId, + 'data-test-subj': `migrationSelectionOption-${index}`, + label: i18n.SIEM_MIGRATIONS_OPTION_LABEL(index + 1), + }, + ] + : []; + }, [migrationsIds, selectedMigrationId]); + + const onChange = (selected: Array>) => { + onMigrationIdChange(selected[0].value); + }; + + if (!migrationsIds.length) { + return null; + } + + return ( + + + + + + ); +}; + +export const HeaderButtons = React.memo(HeaderButtonsComponent); +HeaderButtons.displayName = 'HeaderButtons'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts new file mode 100644 index 0000000000000..e00721c70103a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts @@ -0,0 +1,23 @@ +/* + * 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 SIEM_MIGRATIONS_OPTION_AREAL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.selectionOption.arealLabel', + { + defaultMessage: 'Select a migration', + } +); + +export const SIEM_MIGRATIONS_OPTION_LABEL = (optionIndex: number) => + i18n.translate('xpack.securitySolution.siemMigrations.rules.selectionOption.title', { + defaultMessage: 'SIEM rule migration {optionIndex}', + values: { + optionIndex, + }, + }); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/no_migrations/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/no_migrations/index.tsx new file mode 100644 index 0000000000000..e4b3701d94c73 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/no_migrations/index.tsx @@ -0,0 +1,53 @@ +/* + * 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 { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { SecurityPageName } from '../../../../../common'; +import { useGetSecuritySolutionLinkProps } from '../../../../common/components/links'; +import * as i18n from './translations'; + +const NoMigrationsComponent = () => { + const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); + const { onClick: onClickLink } = getSecuritySolutionLinkProps({ + deepLinkId: SecurityPageName.landing, + path: 'siem_migrations', + }); + + return ( + + + {i18n.NO_MIGRATIONS_AVAILABLE}} + titleSize="s" + body={i18n.NO_MIGRATIONS_AVAILABLE_BODY} + data-test-subj="noMigrationsAvailable" + /> + + + + {i18n.GO_BACK_TO_RULES_TABLE_BUTTON} + + + + ); +}; + +export const NoMigrations = React.memo(NoMigrationsComponent); +NoMigrations.displayName = 'NoMigrations'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/no_migrations/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/no_migrations/translations.ts new file mode 100644 index 0000000000000..77ec0454fb5a5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/no_migrations/translations.ts @@ -0,0 +1,29 @@ +/* + * 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 NO_MIGRATIONS_AVAILABLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.noMigrationsTitle', + { + defaultMessage: 'No migrations', + } +); + +export const NO_MIGRATIONS_AVAILABLE_BODY = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.noMigrationsBodyTitle', + { + defaultMessage: 'There are no migrations available', + } +); + +export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.goToMigrationsPageButton', + { + defaultMessage: 'Go back to SIEM Migrations', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx new file mode 100644 index 0000000000000..5f4ae3098b6a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import type { Dispatch, SetStateAction } from 'react'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import * as i18n from './translations'; +import { RuleSearchField } from '../../../../detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_search_field'; +import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install'; + +const FilterWrapper = styled(EuiFlexGroup)` + margin-bottom: ${({ theme }) => theme.eui.euiSizeM}; +`; + +export interface FiltersComponentProps { + /** + * Currently selected table filter + */ + filterOptions: TableFilterOptions; + + /** + * Handles filter options changes + */ + setFilterOptions: Dispatch>; +} + +/** + * Collection of filters for filtering data within the SIEM Rules Migrations table. + * Contains search bar and tag selection + */ +const FiltersComponent: React.FC = ({ filterOptions, setFilterOptions }) => { + const handleOnSearch = useCallback( + (filterString: string) => { + setFilterOptions((filters) => ({ + ...filters, + filter: filterString.trim(), + })); + }, + [setFilterOptions] + ); + + return ( + + + + ); +}; + +export const Filters = React.memo(FiltersComponent); +Filters.displayName = 'Filters'; 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 new file mode 100644 index 0000000000000..0cd3e07ea11a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx @@ -0,0 +1,125 @@ +/* + * 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 { + EuiInMemoryTable, + EuiSkeletonLoading, + EuiProgress, + EuiSkeletonTitle, + EuiSkeletonText, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import React, { useState } from 'react'; + +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { + RULES_TABLE_INITIAL_PAGE_SIZE, + RULES_TABLE_PAGE_SIZE_OPTIONS, +} from '../../../../detection_engine/rule_management_ui/components/rules_table/constants'; +import { NoItemsMessage } from './no_items_message'; +import { Filters } from './filters'; +import { useRulesTableColumns } from '../../hooks/use_rules_table_columns'; +import { useGetRuleMigrationsQuery } from '../../api/hooks/use_get_rule_migrations'; +import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install'; +import { useFilterRulesToInstall } from '../../hooks/use_filter_rules_to_install'; + +export interface RulesTableComponentProps { + /** + * Selected rule migration id + */ + migrationId: string; + + /** + * Opens the flyout with the details of the rule migration + * @param rule Rule migration + * @returns + */ + openRulePreview: (rule: RuleMigration) => void; +} + +/** + * Table Component for displaying SIEM rules migrations + */ +const RulesTableComponent: React.FC = ({ + migrationId, + openRulePreview, +}) => { + const { data: ruleMigrations, isLoading } = useGetRuleMigrationsQuery(migrationId); + + const [selectedRuleMigrations, setSelectedRuleMigrations] = useState([]); + + const [filterOptions, setFilterOptions] = useState({ + filter: '', + }); + + const filteredRuleMigrations = useFilterRulesToInstall({ + filterOptions, + ruleMigrations: ruleMigrations ?? [], + }); + + const shouldShowProgress = isLoading; + + const rulesColumns = useRulesTableColumns({ + openRulePreview, + }); + + return ( + <> + {shouldShowProgress && ( + + )} + + + + + } + loadedContent={ + !filteredRuleMigrations.length ? ( + + ) : ( + <> + + + + + + + true, + onSelectionChange: setSelectedRuleMigrations, + initialSelected: selectedRuleMigrations, + }} + itemId="rule_id" + data-test-subj="rules-translation-table" + columns={rulesColumns} + /> + + ) + } + /> + + ); +}; + +export const RulesTable = React.memo(RulesTableComponent); +RulesTable.displayName = 'RulesTable'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/no_items_message.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/no_items_message.tsx new file mode 100644 index 0000000000000..7aeaac7ab2f6b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/no_items_message.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { SecurityPageName } from '../../../../../common'; +import { useGetSecuritySolutionLinkProps } from '../../../../common/components/links'; +import * as i18n from './translations'; + +const NoItemsMessageComponent = () => { + const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); + const { onClick: onClickLink } = getSecuritySolutionLinkProps({ + deepLinkId: SecurityPageName.landing, + path: 'siem_migrations', + }); + + return ( + + + {i18n.NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL}} + titleSize="s" + body={i18n.NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL_BODY} + data-test-subj="noRulesTranslationAvailableForInstall" + /> + + + + {i18n.GO_BACK_TO_RULES_TABLE_BUTTON} + + + + ); +}; + +export const NoItemsMessage = React.memo(NoItemsMessageComponent); 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 new file mode 100644 index 0000000000000..812f26f628e49 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.searchBarPlaceholder', + { + defaultMessage: 'Search by rule name', + } +); + +export const NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.noRulesTitle', + { + defaultMessage: 'Empty migration', + } +); + +export const NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL_BODY = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.noRulesBodyTitle', + { + defaultMessage: 'There are no translations available for installation', + } +); + +export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.goToMigrationsPageButton', + { + defaultMessage: 'Go back to SIEM Migrations', + } +); 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 new file mode 100644 index 0000000000000..aaf256cfb60b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.test.tsx @@ -0,0 +1,19 @@ +/* + * 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 new file mode 100644 index 0000000000000..40b3c5ceb5719 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { euiLightVars } from '@kbn/ui-theme'; + +import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; +import { convertTranslationResultIntoText } from '../../utils/helpers'; + +const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars; +const statusToColorMap: Record = { + full: euiColorVis0, + partial: euiColorVis7, + untranslatable: euiColorVis9, +}; + +interface Props { + value?: RuleMigrationTranslationResult; + installedRuleId?: string; + 'data-test-subj'?: string; +} + +const StatusBadgeComponent: React.FC = ({ + value, + installedRuleId, + 'data-test-subj': dataTestSubj = 'translation-result', +}) => { + const translationResult = installedRuleId || !value ? 'full' : value; + const displayValue = convertTranslationResultIntoText(translationResult); + const color = statusToColorMap[translationResult]; + + return ( + + {displayValue} + + ); +}; + +export const StatusBadge = React.memo(StatusBadgeComponent); +StatusBadge.displayName = 'StatusBadge'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/constants.ts new file mode 100644 index 0000000000000..4d6bcd542b866 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/constants.ts @@ -0,0 +1,9 @@ +/* + * 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 DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['50%', '50%']; +export const LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['30%', '70%']; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx new file mode 100644 index 0000000000000..4aaff21281d64 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx @@ -0,0 +1,246 @@ +/* + * 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 { FC, PropsWithChildren } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { css } from '@emotion/css'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { + EuiButtonEmpty, + EuiTitle, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTabbedContent, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui'; + +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 * as i18n from './translations'; +import { + DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS, + LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS, +} from './constants'; +import { TranslationTab } from './translation_tab'; +import { + DEFAULT_TRANSLATION_RISK_SCORE, + DEFAULT_TRANSLATION_SEVERITY, +} from '../../utils/constants'; + +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + .euiFlyoutBody__overflow { + display: flex; + flex: 1; + overflow: hidden; + + .euiFlyoutBody__overflowContent { + flex: 1; + overflow: hidden; + padding: ${({ theme }) => `0 ${theme.eui.euiSizeL} 0`}; + } + } +`; + +const StyledFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +const StyledEuiFlexItem = styled(EuiFlexItem)` + &.euiFlexItem { + flex: 1 0 0; + overflow: hidden; + } +`; + +const StyledEuiTabbedContent = styled(EuiTabbedContent)` + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; + + > [role='tabpanel'] { + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; + overflow-y: auto; + + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; + } + + ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); + } + } +`; + +/* + * Fixes tabs to the top and allows the content to scroll. + */ +const ScrollableFlyoutTabbedContent = (props: EuiTabbedContentProps) => ( + + + + + +); + +const tabPaddingClassName = css` + padding: 0 ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeXL} ${euiThemeVars.euiSizeM}; +`; + +export const TabContentPadding: FC> = ({ children }) => ( +
{children}
+); + +interface TranslationDetailsFlyoutProps { + ruleActions?: React.ReactNode; + ruleMigration: RuleMigration; + size?: EuiFlyoutProps['size']; + extraTabs?: EuiTabbedContentTab[]; + closeFlyout: () => void; +} + +export const TranslationDetailsFlyout = ({ + ruleActions, + ruleMigration, + size = 'm', + extraTabs = [], + closeFlyout, +}: TranslationDetailsFlyoutProps) => { + 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 values + severity: (ruleMigration.elastic_rule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY, + risk_score: DEFAULT_TRANSLATION_RISK_SCORE, + from: 'now-360s', + to: 'now', + interval: '5m', + } as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter + }, [ruleMigration]); + + const translationTab: EuiTabbedContentTab = useMemo( + () => ({ + id: 'translation', + name: i18n.TRANSLATION_TAB_LABEL, + content: ( + + + + ), + }), + [ruleMigration] + ); + + const overviewTab: EuiTabbedContentTab = useMemo( + () => ({ + id: 'overview', + name: i18n.OVERVIEW_TAB_LABEL, + content: ( + + + + ), + }), + [rule, size, expandedOverviewSections, toggleOverviewSection] + ); + + const tabs = useMemo(() => { + return [...extraTabs, translationTab, overviewTab]; + }, [extraTabs, translationTab, overviewTab]); + + const [selectedTabId, setSelectedTabId] = useState(tabs[0].id); + const selectedTab = tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0]; + + useEffect(() => { + if (!tabs.find((tab) => tab.id === selectedTabId)) { + // Switch to first tab if currently selected tab is not available for this rule + setSelectedTabId(tabs[0].id); + } + }, [tabs, selectedTabId]); + + const onTabClick = (tab: EuiTabbedContentTab) => { + setSelectedTabId(tab.id); + }; + + const migrationsRulesFlyoutTitleId = useGeneratedHtmlId({ + prefix: 'migrationRulesFlyoutTitle', + }); + + return ( + + + +

{rule.name}

+
+ +
+ + + + + + + + {i18n.DISMISS_BUTTON_LABEL} + + + {ruleActions} + + +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/header.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/header.tsx new file mode 100644 index 0000000000000..57e99440e60a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/header.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiTitle } from '@elastic/eui'; +import * as i18n from './translations'; + +export function TranslationTabHeader(): JSX.Element { + return ( + + +
{i18n.TAB_HEADER_TITLE}
+
+
+ ); +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/index.tsx new file mode 100644 index 0000000000000..66836b8ea5631 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/index.tsx @@ -0,0 +1,110 @@ +/* + * 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 { + EuiAccordion, + EuiBadge, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiSplitPanel, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { TranslationTabHeader } from './header'; +import { RuleQueryComponent } from './rule_query'; +import * as i18n from './translations'; +import { convertTranslationResultIntoText } from '../../../utils/helpers'; + +interface TranslationTabProps { + ruleMigration: RuleMigration; +} + +export const TranslationTab = ({ ruleMigration }: TranslationTabProps) => { + 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'; + + return ( + <> + + + + + + } + initialIsOpen={true} + > + + + + + + + +

+ +

+
+
+ + {}} + onClickAriaLabel={'Click to update translation status'} + > + {convertTranslationResultIntoText(ruleMigration.translation_result)} + + +
+
+ + + + + + + + + + + +
+
+
+ + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/rule_query.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/rule_query.tsx new file mode 100644 index 0000000000000..50977cafb18d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/rule_query.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiMarkdownEditor, EuiMarkdownFormat, EuiTitle } from '@elastic/eui'; +import { SideHeader } from '../../../../../detection_engine/rule_management/components/rule_details/three_way_diff/components/side_header'; +import { FinalSideHelpInfo } from '../../../../../detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info'; +import * as i18n from './translations'; + +interface RuleQueryProps { + title: string; + query: string; + canEdit?: boolean; +} + +export const RuleQueryComponent = ({ title, query, canEdit }: RuleQueryProps) => { + const queryTextComponent = useMemo(() => { + if (canEdit) { + return ( + {}} + height={400} + initialViewMode={'viewing'} + /> + ); + } else { + return {query}; + } + }, [canEdit, query]); + return ( + <> + + +

+ {title} + +

+
+
+ {queryTextComponent} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/translations.ts new file mode 100644 index 0000000000000..e7532a5a8b2e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/translations.ts @@ -0,0 +1,43 @@ +/* + * 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 TAB_HEADER_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.title', + { + defaultMessage: 'Translation', + } +); + +export const NAME_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.nameLabel', + { + defaultMessage: 'Name', + } +); + +export const SPLUNK_QUERY_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.splunkQueryTitle', + { + defaultMessage: 'Splunk query', + } +); + +export const ESQL_TRANSLATION_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.esqlTranslationTitle', + { + defaultMessage: 'ES|QL translation', + } +); + +export const TRANSLATED_QUERY_AREAL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.queryArealLabel', + { + defaultMessage: 'Translated query', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translations.ts new file mode 100644 index 0000000000000..8e6582b8c198e --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translations.ts @@ -0,0 +1,29 @@ +/* + * 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 OVERVIEW_TAB_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.overviewTabLabel', + { + defaultMessage: 'Overview', + } +); + +export const TRANSLATION_TAB_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTabLabel', + { + defaultMessage: 'Translation', + } +); + +export const DISMISS_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.dismissButtonLabel', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/translations.ts new file mode 100644 index 0000000000000..74845b5f257ad --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/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 COLUMN_STATUS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.columns.statusTitle', + { + defaultMessage: 'Status', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_filter_rules_to_install.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_filter_rules_to_install.ts new file mode 100644 index 0000000000000..f6862d3d90380 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_filter_rules_to_install.ts @@ -0,0 +1,33 @@ +/* + * 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 { useMemo } from 'react'; +import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import type { FilterOptions } from '../../../detection_engine/rule_management/logic/types'; + +export type TableFilterOptions = Pick; + +export const useFilterRulesToInstall = ({ + ruleMigrations, + filterOptions, +}: { + ruleMigrations: RuleMigration[]; + filterOptions: TableFilterOptions; +}) => { + const filteredRules = useMemo(() => { + const { filter } = filterOptions; + return ruleMigrations.filter((migration) => { + const name = migration.elastic_rule?.title ?? migration.original_rule.title; + if (filter && !name.toLowerCase().includes(filter.toLowerCase())) { + return false; + } + return true; + }); + }, [filterOptions, ruleMigrations]); + + return filteredRules; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rule_preview_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rule_preview_flyout.tsx new file mode 100644 index 0000000000000..1721b4e280aad --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rule_preview_flyout.tsx @@ -0,0 +1,55 @@ +/* + * 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 { 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 { TranslationDetailsFlyout } from '../components/translation_details_flyout'; + +interface UseRulePreviewFlyoutParams { + ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode; + extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[]; +} + +interface UseRulePreviewFlyoutResult { + rulePreviewFlyout: ReactNode; + openRulePreview: (rule: RuleMigration) => void; + closeRulePreview: () => void; +} + +export function useRulePreviewFlyout({ + extraTabsFactory, + ruleActionsFactory, +}: UseRulePreviewFlyoutParams): UseRulePreviewFlyoutResult { + const [ruleMigration, setRuleMigrationForPreview] = useState(); + const closeRulePreview = useCallback(() => setRuleMigrationForPreview(undefined), []); + const ruleActions = useMemo( + () => ruleMigration && ruleActionsFactory(ruleMigration, closeRulePreview), + [ruleMigration, ruleActionsFactory, closeRulePreview] + ); + const extraTabs = useMemo( + () => (ruleMigration && extraTabsFactory ? extraTabsFactory(ruleMigration) : []), + [ruleMigration, extraTabsFactory] + ); + + return { + rulePreviewFlyout: ruleMigration && ( + + ), + openRulePreview: useCallback((rule: RuleMigration) => { + setRuleMigrationForPreview(rule); + }, []), + closeRulePreview, + }; +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx new file mode 100644 index 0000000000000..3b13b9e631ccb --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx @@ -0,0 +1,107 @@ +/* + * 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 { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiText, EuiLink } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import { SeverityBadge } from '../../../common/components/severity_badge'; +import * as rulesI18n from '../../../detections/pages/detection_engine/rules/translations'; +import * as i18n from './translations'; +import { getNormalizedSeverity } from '../../../detection_engine/rule_management_ui/components/rules_table/helpers'; +import { StatusBadge } from '../components/status_badge'; +import { DEFAULT_TRANSLATION_RISK_SCORE, DEFAULT_TRANSLATION_SEVERITY } from '../utils/constants'; + +export type TableColumn = EuiBasicTableColumn; + +interface RuleNameProps { + name: string; + rule: RuleMigration; + openRulePreview: (rule: RuleMigration) => void; +} + +const RuleName = ({ name, rule, openRulePreview }: RuleNameProps) => { + return ( + { + openRulePreview(rule); + }} + data-test-subj="ruleName" + > + {name} + + ); +}; + +const createRuleNameColumn = ({ + openRulePreview, +}: { + openRulePreview: (rule: RuleMigration) => void; +}): TableColumn => { + return { + field: 'original_rule.title', + name: rulesI18n.COLUMN_RULE, + render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => ( + + ), + sortable: true, + truncateText: true, + width: '40%', + align: 'left', + }; +}; + +const STATUS_COLUMN: TableColumn = { + field: 'translation_result', + name: i18n.COLUMN_STATUS, + render: (value: RuleMigration['translation_result'], rule: RuleMigration) => ( + + ), + sortable: false, + truncateText: true, + width: '12%', +}; + +export const useRulesTableColumns = ({ + openRulePreview, +}: { + openRulePreview: (rule: RuleMigration) => void; +}): TableColumn[] => { + return useMemo( + () => [ + createRuleNameColumn({ openRulePreview }), + STATUS_COLUMN, + { + field: 'risk_score', + name: rulesI18n.COLUMN_RISK_SCORE, + render: () => ( + + {DEFAULT_TRANSLATION_RISK_SCORE} + + ), + sortable: true, + truncateText: true, + width: '75px', + }, + { + field: 'elastic_rule.severity', + name: rulesI18n.COLUMN_SEVERITY, + render: (value?: Severity) => ( + + ), + sortable: ({ elastic_rule: elasticRule }: RuleMigration) => + getNormalizedSeverity( + (elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY + ), + truncateText: true, + width: '12%', + }, + ], + [openRulePreview] + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx new file mode 100644 index 0000000000000..616c85e7e7dee --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -0,0 +1,104 @@ +/* + * 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, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui'; +import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import { SecurityPageName } from '../../../app/types'; +import { HeaderPage } from '../../../common/components/header_page'; +import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; + +import * as i18n from './translations'; +import { RulesTable } from '../components/rules_table'; +import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/callouts/need_admin_for_update_callout'; +import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout'; +import { HeaderButtons } from '../components/header_buttons'; +import { useGetRuleMigrationsStatsAllQuery } from '../api/hooks/use_get_rule_migrations_stats_all'; +import { useRulePreviewFlyout } from '../hooks/use_rule_preview_flyout'; +import { NoMigrations } from '../components/no_migrations'; + +const RulesPageComponent: React.FC = () => { + const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } = + useGetRuleMigrationsStatsAllQuery(); + + const migrationsIds = useMemo(() => { + if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) { + return []; + } + return ruleMigrationsStatsAll + .filter((migration) => migration.status === 'finished') + .map((migration) => migration.migration_id); + }, [isLoadingMigrationsStats, ruleMigrationsStatsAll]); + + const [selectedMigrationId, setSelectedMigrationId] = useState(); + const onMigrationIdChange = (selectedId?: string) => { + setSelectedMigrationId(selectedId); + }; + + useEffect(() => { + if (!migrationsIds.length) { + return; + } + const index = migrationsIds.findIndex((id) => id === selectedMigrationId); + if (index === -1) { + setSelectedMigrationId(migrationsIds[0]); + } + }, [migrationsIds, selectedMigrationId]); + + const ruleActionsFactory = useCallback( + (ruleMigration: RuleMigration, closeRulePreview: () => void) => { + // TODO: Add flyout action buttons + return null; + }, + [] + ); + + const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ + ruleActionsFactory, + }); + + return ( + <> + + + + + + + + + + + + } + loadedContent={ + selectedMigrationId ? ( + + ) : ( + + ) + } + /> + {rulePreviewFlyout} + + + + + ); +}; + +export const RulesPage = React.memo(RulesPageComponent); +RulesPage.displayName = 'RulesPage'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/translations.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/translations.tsx new file mode 100644 index 0000000000000..3c95eaab8fe90 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/translations.tsx @@ -0,0 +1,12 @@ +/* + * 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 PAGE_TITLE = i18n.translate('xpack.securitySolution.siemMigrations.rules.pageTitle', { + defaultMessage: 'Translated rules', +}); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts new file mode 100644 index 0000000000000..7400d4b0bcb63 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts @@ -0,0 +1,11 @@ +/* + * 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 '@kbn/securitysolution-io-ts-alerting-types'; + +export const DEFAULT_TRANSLATION_RISK_SCORE = 21; +export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx new file mode 100644 index 0000000000000..cd49311db21eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx @@ -0,0 +1,28 @@ +/* + * 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 { + RuleMigrationTranslationResultEnum, + type RuleMigrationTranslationResult, +} from '../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; + +export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => { + switch (status) { + case RuleMigrationTranslationResultEnum.full: + return i18n.SIEM_TRANSLATION_RESULT_FULL_LABEL; + + case RuleMigrationTranslationResultEnum.partial: + return i18n.SIEM_TRANSLATION_RESULT_PARTIAL_LABEL; + + case RuleMigrationTranslationResultEnum.untranslatable: + return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL; + + default: + return i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL; + } +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts new file mode 100644 index 0000000000000..bc098936c00f7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const SIEM_TRANSLATION_RESULT_FULL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationResult.full', + { + defaultMessage: 'Fully translated', + } +); + +export const SIEM_TRANSLATION_RESULT_PARTIAL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationResult.partially', + { + defaultMessage: 'Partially translated', + } +); + +export const SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationResult.untranslatable', + { + defaultMessage: 'Not translated', + } +); + +export const SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationResult.unknown', + { + defaultMessage: 'Unknown', + } +); diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 6642e02d5ecd6..9829fe2ad0c25 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -83,6 +83,7 @@ import type { EntityAnalytics } from './entity_analytics'; import type { Assets } from './assets'; import type { Investigations } from './investigations'; import type { MachineLearning } from './machine_learning'; +import type { SiemMigrations } from './siem_migrations'; import type { Dashboards } from './dashboards'; import type { BreadcrumbsNav } from './common/breadcrumbs/types'; @@ -243,6 +244,7 @@ export interface SubPlugins { assets: Assets; investigations: Investigations; machineLearning: MachineLearning; + siemMigrations: SiemMigrations; } // TODO: find a better way to defined these types @@ -266,4 +268,5 @@ export interface StartedSubPlugins { assets: ReturnType; investigations: ReturnType; machineLearning: ReturnType; + siemMigrations: ReturnType; }