diff --git a/packages/deeplinks/security/deep_links.ts b/packages/deeplinks/security/deep_links.ts index 644691bd5b8bc..1da816c63ce2f 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', + rulesSiemMigrations = 'rules-siem-migrations', /* * 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..73e9ef7c39589 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -98,6 +98,7 @@ export const RULES_LANDING_PATH = `${RULES_PATH}/landing` as const; export const RULES_ADD_PATH = `${RULES_PATH}/add_rules` as const; export const RULES_UPDATES = `${RULES_PATH}/updates` as const; export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const; +export const SIEM_MIGRATIONS_PATH = `${RULES_PATH}/siem_migrations` as const; export const EXCEPTIONS_PATH = '/exceptions' as const; export const EXCEPTION_LIST_DETAIL_PATH = `${EXCEPTIONS_PATH}/details/:detailName` as const; export const HOSTS_PATH = '/hosts' as const; diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 709bb5f614f7b..2dae25954c3a9 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -101,6 +101,10 @@ export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exce defaultMessage: 'Shared exception lists', }); +export const SIEM_MIGRATIONS = i18n.translate('xpack.securitySolution.navigation.siemMigrations', { + defaultMessage: 'SIEM Rules Migrations', +}); + export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', { defaultMessage: 'Alerts', }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index aea4b6672659e..b7d4b59cc54bb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -13,6 +13,15 @@ import { INTERNAL_ALERTING_API_FIND_RULES_PATH } from '@kbn/alerting-plugin/comm import { BASE_ACTION_API_PATH } from '@kbn/actions-plugin/common'; import type { ActionType, AsApiContract } from '@kbn/actions-plugin/common'; import type { ActionResult } from '@kbn/actions-plugin/server'; +import { replaceParams } from '@kbn/openapi-common/shared'; +import type { + GetAllStatsRuleMigrationResponse, + GetRuleMigrationResponse, +} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, + SIEM_RULE_MIGRATION_PATH, +} from '../../../../common/siem_migrations/constants'; import { convertRulesFilterToKQL } from '../../../../common/detection_engine/rule_management/rule_filtering'; import type { UpgradeSpecificRulesRequest, @@ -698,3 +707,56 @@ export const bootstrapPrebuiltRules = async (): Promise => + 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 => { + if (!migrationId) { + return []; + } + 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/detection_engine/rule_management/api/hooks/siem_migrations/use_get_rule_migrations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/siem_migrations/use_get_rule_migrations.ts new file mode 100644 index 0000000000000..a295c376c30e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/siem_migrations/use_get_rule_migrations.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 type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { replaceParams } from '@kbn/openapi-common/shared'; +import type { GetRuleMigrationResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATION_PATH } from '../../../../../../common/siem_migrations/constants'; +import { getRuleMigrations } from '../../api'; +import { DEFAULT_QUERY_OPTIONS } from '../constants'; + +export const useGetRuleMigrationsQuery = ( + migrationId?: string, + options?: UseQueryOptions +) => { + const SPECIFIC_MIGRATION_PATH = migrationId + ? replaceParams(SIEM_RULE_MIGRATION_PATH, { + migration_id: migrationId, + }) + : SIEM_RULE_MIGRATION_PATH; + 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/detection_engine/rule_management/api/hooks/siem_migrations/use_get_rule_migrations_stats_all.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/siem_migrations/use_get_rule_migrations_stats_all.ts new file mode 100644 index 0000000000000..bb87f874632ec --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/siem_migrations/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 type { GetAllStatsRuleMigrationResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATIONS_ALL_STATS_PATH } from '../../../../../../common/siem_migrations/constants'; +import { getRuleMigrationsStatsAll } from '../../api'; +import { DEFAULT_QUERY_OPTIONS } from '../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/detection_engine/rule_management_ui/components/siem_migrations/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/constants.ts new file mode 100644 index 0000000000000..7400d4b0bcb63 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/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/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_header_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_header_buttons.tsx new file mode 100644 index 0000000000000..80ed7eb8983ee --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_header_buttons.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { useSiemMigrationsTableContext } from './siem_migrations_table_context'; +import * as i18n from './translations'; + +export const SiemMigrationsHeaderButtons = () => { + const { + state: { migrationsIds, selectedMigrationId }, + actions: { onMigrationIdChange }, + } = useSiemMigrationsTableContext(); + + 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 ( + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_no_items_message.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_no_items_message.tsx new file mode 100644 index 0000000000000..291fe8e49f400 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_no_items_message.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 SiemMigrationsTableNoItemsMessageComponent = () => { + const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); + const { onClick: onClickLink } = getSecuritySolutionLinkProps({ + deepLinkId: SecurityPageName.rules, + }); + + 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 SiemMigrationsTableNoItemsMessage = React.memo( + SiemMigrationsTableNoItemsMessageComponent +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table.tsx new file mode 100644 index 0000000000000..79769fcb3d461 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table.tsx @@ -0,0 +1,95 @@ +/* + * 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 from 'react'; + +import { + RULES_TABLE_INITIAL_PAGE_SIZE, + RULES_TABLE_PAGE_SIZE_OPTIONS, +} from '../rules_table/constants'; +import { SiemMigrationsTableNoItemsMessage } from './siem_migrations_no_items_message'; +import { useSiemMigrationsTableContext } from './siem_migrations_table_context'; +import { SiemMigrationsTableFilters } from './siem_migrations_table_filters'; +import { useSiemMigrationsTableColumns } from './use_siem_migrations_table_columns'; + +/** + * Table Component for displaying SIEM rules migrations + */ +export const SiemMigrationsTable = React.memo(() => { + const siemMigrationsTableContext = useSiemMigrationsTableContext(); + + const { + state: { ruleMigrations, isLoading, selectedRuleMigrations }, + actions: { selectRuleMigrations }, + } = siemMigrationsTableContext; + const rulesColumns = useSiemMigrationsTableColumns(); + + const shouldShowProgress = isLoading; + + return ( + <> + {shouldShowProgress && ( + + )} + + + + + } + loadedContent={ + !ruleMigrations.length ? ( + + ) : ( + <> + + + + + + + true, + onSelectionChange: selectRuleMigrations, + initialSelected: selectedRuleMigrations, + }} + itemId="rule_id" + data-test-subj="rules-translation-table" + columns={rulesColumns} + /> + + ) + } + /> + + ); +}); + +SiemMigrationsTable.displayName = 'SiemMigrationsTable'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table_context.tsx new file mode 100644 index 0000000000000..8b66dbd4a8485 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table_context.tsx @@ -0,0 +1,188 @@ +/* + * 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 { Dispatch, SetStateAction } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { useGetRuleMigrationsStatsAllQuery } from '../../../rule_management/api/hooks/siem_migrations/use_get_rule_migrations_stats_all'; +import { useGetRuleMigrationsQuery } from '../../../rule_management/api/hooks/siem_migrations/use_get_rule_migrations'; +import type { RuleSignatureId } from '../../../../../common/api/detection_engine'; +import { invariant } from '../../../../../common/utils/invariant'; +import type { SiemMigrationsTableFilterOptions } from './use_filter_siem_migrations_to_install'; +import { useFilterSiemMigrationsToInstall } from './use_filter_siem_migrations_to_install'; +import { useSiemMigrationsPreviewFlyout } from './use_siem_migrations_preview_flyout'; + +export interface SiemMigrationsTableState { + /** + * Available rule migrations ids + */ + migrationsIds: string[]; + /** + * Selected rule migration id + */ + selectedMigrationId: string | undefined; + /** + * Rule migrations available after applying `filterOptions` + */ + ruleMigrations: RuleMigration[]; + /** + * Currently selected table filter + */ + filterOptions: SiemMigrationsTableFilterOptions; + /** + * All unique tags for all rule translations + */ + tags: string[]; + /** + * If true then there is no cached data and the query is currently fetching. + */ + isLoading: boolean; + /** + * Rule rows selected in EUI InMemory Table + */ + selectedRuleMigrations: RuleMigration[]; +} + +export interface SiemMigrationsTableActions { + setFilterOptions: Dispatch>; + selectRuleMigrations: (rules: RuleMigration[]) => void; + openRuleTranslationPreview: (ruleId: RuleSignatureId) => void; + onMigrationIdChange: (selectedId?: string) => void; +} + +export interface SiemMigrationsContextType { + state: SiemMigrationsTableState; + actions: SiemMigrationsTableActions; +} + +const SiemMigrationsTableContext = createContext(null); + +interface SiemMigrationsTableContextProviderProps { + children: React.ReactNode; +} + +const SIEM_MIGRATIONS_INSTALL_FLYOUT_ANCHOR = 'installSiemMigrationsPreview'; + +export const SiemMigrationsTableContextProvider = ({ + children, +}: SiemMigrationsTableContextProviderProps) => { + const [selectedRuleMigrations, setSelectedRuleMigrations] = useState([]); + + const [filterOptions, setFilterOptions] = useState({ + filter: '', + tags: [], + }); + + 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 { data: ruleMigrations, isLoading: isLoadingRuleMigrations } = + useGetRuleMigrationsQuery(selectedMigrationId); + + const filteredRuleMigrations = useFilterSiemMigrationsToInstall({ + filterOptions, + ruleMigrations: ruleMigrations ?? [], + }); + + const tags = useMemo(() => [], []); + + const ruleActionsFactory = useCallback( + (ruleMigration: RuleMigration, closeRuleMigrationPreview: () => void) => { + // TODO: Add flyout action buttons + return null; + }, + [] + ); + + const { ruleMigrationPreviewFlyout, openRuleTranslationPreview } = useSiemMigrationsPreviewFlyout( + { + ruleMigrations: filteredRuleMigrations, + ruleActionsFactory, + flyoutProps: { + id: SIEM_MIGRATIONS_INSTALL_FLYOUT_ANCHOR, + dataTestSubj: SIEM_MIGRATIONS_INSTALL_FLYOUT_ANCHOR, + }, + } + ); + + const actions = useMemo( + () => ({ + setFilterOptions, + selectRuleMigrations: setSelectedRuleMigrations, + openRuleTranslationPreview, + onMigrationIdChange, + }), + [openRuleTranslationPreview] + ); + + const providerValue = useMemo(() => { + return { + state: { + migrationsIds, + selectedMigrationId, + ruleMigrations: filteredRuleMigrations, + filterOptions, + tags, + isLoading: isLoadingMigrationsStats || isLoadingRuleMigrations, + selectedRuleMigrations, + }, + actions, + }; + }, [ + migrationsIds, + selectedMigrationId, + filteredRuleMigrations, + filterOptions, + tags, + isLoadingMigrationsStats, + isLoadingRuleMigrations, + selectedRuleMigrations, + actions, + ]); + + return ( + + <> + {children} + {ruleMigrationPreviewFlyout} + + + ); +}; + +export const useSiemMigrationsTableContext = (): SiemMigrationsContextType => { + const rulesTableContext = useContext(SiemMigrationsTableContext); + invariant( + rulesTableContext, + 'useSiemMigrationsTableContext should be used inside SiemMigrationsTableContextProvider' + ); + + return rulesTableContext; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table_filters.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table_filters.tsx new file mode 100644 index 0000000000000..a8e3f6764d2b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/siem_migrations_table_filters.tsx @@ -0,0 +1,80 @@ +/* + * 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 { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import * as i18n from './translations'; +import { TagsFilterPopover } from '../rules_table/rules_table_filters/tags_filter_popover'; +import { RuleSearchField } from '../rules_table/rules_table_filters/rule_search_field'; +import { useSiemMigrationsTableContext } from './siem_migrations_table_context'; + +const FilterWrapper = styled(EuiFlexGroup)` + margin-bottom: ${({ theme }) => theme.eui.euiSizeM}; +`; + +/** + * Collection of filters for filtering data within the SIEM Rules Migrations table. + * Contains search bar and tag selection + */ +const SiemMigrationsTableFiltersComponent = () => { + const { + state: { filterOptions, tags }, + actions: { setFilterOptions }, + } = useSiemMigrationsTableContext(); + + const { tags: selectedTags } = filterOptions; + + const handleOnSearch = useCallback( + (filterString: string) => { + setFilterOptions((filters) => ({ + ...filters, + filter: filterString.trim(), + })); + }, + [setFilterOptions] + ); + + const handleSelectedTags = useCallback( + (newTags: string[]) => { + if (!isEqual(newTags, selectedTags)) { + setFilterOptions((filters) => ({ + ...filters, + tags: newTags, + })); + } + }, + [selectedTags, setFilterOptions] + ); + + return ( + + + + + + + + + ); +}; + +SiemMigrationsTableFiltersComponent.displayName = 'SiemMigrationsTableFiltersComponent'; + +export const SiemMigrationsTableFilters = React.memo(SiemMigrationsTableFiltersComponent); + +SiemMigrationsTableFilters.displayName = 'SiemMigrationsTableFilters'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/status_badge/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/status_badge/index.test.tsx new file mode 100644 index 0000000000000..aaf256cfb60b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/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/detection_engine/rule_management_ui/components/siem_migrations/status_badge/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/status_badge/index.tsx new file mode 100644 index 0000000000000..1088805c127f2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/status_badge/index.tsx @@ -0,0 +1,48 @@ +/* + * 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'; + +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); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/constants.ts new file mode 100644 index 0000000000000..4d6bcd542b866 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/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/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_details_flyout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_details_flyout.tsx new file mode 100644 index 0000000000000..3a0793a320ebc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_details_flyout.tsx @@ -0,0 +1,248 @@ +/* + * 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 '../../../../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 '../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[]; + dataTestSubj?: string; + id?: string; + closeFlyout: () => void; +} + +export const TranslationDetailsFlyout = ({ + ruleActions, + ruleMigration, + size = 'm', + extraTabs = [], + dataTestSubj, + id, + 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/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_tab/header.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_tab/header.tsx new file mode 100644 index 0000000000000..57e99440e60a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/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/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_tab/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_tab/index.tsx new file mode 100644 index 0000000000000..5772b737e227b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/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'; + +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/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_tab/rule_query.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_tab/rule_query.tsx new file mode 100644 index 0000000000000..ee66cf0f0be8d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/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 '../../../../../rule_management/components/rule_details/three_way_diff/components/side_header'; +import { FinalSideHelpInfo } from '../../../../../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/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_tab/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translation_tab/translations.ts new file mode 100644 index 0000000000000..782c1fba8a1bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/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.detectionEngine.translationDetails.translationTab.title', + { + defaultMessage: 'Translation', + } +); + +export const NAME_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.translationDetails.translationTab.nameLabel', + { + defaultMessage: 'Name', + } +); + +export const SPLUNK_QUERY_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.translationDetails.translationTab.splunkQueryTitle', + { + defaultMessage: 'Splunk query', + } +); + +export const ESQL_TRANSLATION_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.translationDetails.translationTab.esqlTranslationTitle', + { + defaultMessage: 'ES|QL translation', + } +); + +export const TRANSLATED_QUERY_AREAL_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.translationDetails.translationTab.queryArealLabel', + { + defaultMessage: 'Translated query', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/translations.ts new file mode 100644 index 0000000000000..fe5ca4700a301 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translation_details/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.detectionEngine.translationDetails.overviewTabLabel', + { + defaultMessage: 'Overview', + } +); + +export const TRANSLATION_TAB_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.translationDetails.translationTabLabel', + { + defaultMessage: 'Translation', + } +); + +export const DISMISS_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.translationDetails.dismissButtonLabel', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translations.ts new file mode 100644 index 0000000000000..60d4662f90646 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/translations.ts @@ -0,0 +1,89 @@ +/* + * 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.detectionEngine.rules.siemMigrations.searchBarPlaceholder', + { + defaultMessage: 'Search by rule name', + } +); + +export const SIEM_TRANSLATION_RESULT_FULL_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.translationResult.full', + { + defaultMessage: 'Fully translated', + } +); + +export const SIEM_TRANSLATION_RESULT_PARTIAL_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.translationResult.partially', + { + defaultMessage: 'Partially translated', + } +); + +export const SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.translationResult.untranslatable', + { + defaultMessage: 'Not translated', + } +); + +export const SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.translationResult.unknown', + { + defaultMessage: 'Unknown', + } +); + +export const NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.noRulesTitle', + { + defaultMessage: 'Empty migration', + } +); + +export const NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL_BODY = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.noRulesBodyTitle', + { + defaultMessage: 'There are no translations available for installation', + } +); + +export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.goToMigrationsPageButton', + { + defaultMessage: 'Go back to SIEM Migrations', + } +); + +export const COLUMN_STATUS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.columns.statusTitle', + { + defaultMessage: 'Status', + } +); + +export const SIEM_MIGRATIONS_OPTION_AREAL_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.selectionOption.arealLabel', + { + defaultMessage: 'Select a migration', + } +); + +export const SIEM_MIGRATIONS_OPTION_LABEL = (optionIndex: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.selectionOption.title', + { + defaultMessage: 'SIEM rule migration {optionIndex}', + values: { + optionIndex, + }, + } + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_filter_siem_migrations_to_install.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_filter_siem_migrations_to_install.ts new file mode 100644 index 0000000000000..ef9f4a7369dc6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_filter_siem_migrations_to_install.ts @@ -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 { useMemo } from 'react'; +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { FilterOptions } from '../../../rule_management/logic/types'; + +export type SiemMigrationsTableFilterOptions = Pick; + +export const useFilterSiemMigrationsToInstall = ({ + ruleMigrations, + filterOptions, +}: { + ruleMigrations: RuleMigration[]; + filterOptions: SiemMigrationsTableFilterOptions; +}) => { + const filteredRules = useMemo(() => { + const { filter, tags } = filterOptions; + return ruleMigrations.filter((migration) => { + const name = migration.elastic_rule?.title ?? migration.original_rule.title; + if (filter && !name.toLowerCase().includes(filter.toLowerCase())) { + return false; + } + + if (tags && tags.length > 0) { + // TODO: Uncomment this once tags are available for rule migrations + // return tags.every((tag) => migration.tags.includes(tag)); + } + + return true; + }); + }, [filterOptions, ruleMigrations]); + + return filteredRules; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_siem_migrations_preview_flyout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_siem_migrations_preview_flyout.tsx new file mode 100644 index 0000000000000..ede9acad5798a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_siem_migrations_preview_flyout.tsx @@ -0,0 +1,82 @@ +/* + * 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 { invariant } from '../../../../../common/utils/invariant'; +import type { RuleSignatureId } from '../../../../../common/api/detection_engine'; +import { TranslationDetailsFlyout } from './translation_details/translation_details_flyout'; + +interface UseSiemMigrationsPreviewFlyoutParams { + ruleMigrations: RuleMigration[]; + ruleActionsFactory: ( + ruleMigration: RuleMigration, + closeRuleMigrationPreview: () => void + ) => ReactNode; + extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[]; + flyoutProps: SiemMigrationsPreviewFlyoutProps; +} + +interface SiemMigrationsPreviewFlyoutProps { + /** + * Rule preview flyout unique id used in HTML + */ + id: string; + dataTestSubj: string; +} + +interface UseSiemMigrationsPreviewFlyoutResult { + ruleMigrationPreviewFlyout: ReactNode; + openRuleTranslationPreview: (ruleId: RuleSignatureId) => void; + closeRuleMigrationPreview: () => void; +} + +export function useSiemMigrationsPreviewFlyout({ + ruleMigrations, + extraTabsFactory, + ruleActionsFactory, + flyoutProps, +}: UseSiemMigrationsPreviewFlyoutParams): UseSiemMigrationsPreviewFlyoutResult { + const [ruleMigration, setRuleMigrationForPreview] = useState(); + const closeRuleMigrationPreview = useCallback(() => setRuleMigrationForPreview(undefined), []); + const ruleActions = useMemo( + () => ruleMigration && ruleActionsFactory(ruleMigration, closeRuleMigrationPreview), + [ruleMigration, ruleActionsFactory, closeRuleMigrationPreview] + ); + const extraTabs = useMemo( + () => (ruleMigration && extraTabsFactory ? extraTabsFactory(ruleMigration) : []), + [ruleMigration, extraTabsFactory] + ); + + return { + ruleMigrationPreviewFlyout: ruleMigration && ( + + ), + openRuleTranslationPreview: useCallback( + (ruleId: RuleSignatureId) => { + const ruleMigrationToShowInFlyout = ruleMigrations.find( + (x) => x.original_rule.id === ruleId + ); + + invariant(ruleMigrationToShowInFlyout, `Rule migration with id ${ruleId} not found`); + setRuleMigrationForPreview(ruleMigrationToShowInFlyout); + }, + [ruleMigrations] + ), + closeRuleMigrationPreview, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_siem_migrations_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_siem_migrations_table_columns.tsx new file mode 100644 index 0000000000000..63a2cb9eee93f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/use_siem_migrations_table_columns.tsx @@ -0,0 +1,101 @@ +/* + * 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 { useSiemMigrationsTableContext } from './siem_migrations_table_context'; +import { getNormalizedSeverity } from '../rules_table/helpers'; +import { StatusBadge } from './status_badge'; +import { DEFAULT_TRANSLATION_RISK_SCORE, DEFAULT_TRANSLATION_SEVERITY } from './constants'; + +export type TableColumn = EuiBasicTableColumn; + +interface RuleNameProps { + name: string; + ruleId: string; +} + +const RuleName = ({ name, ruleId }: RuleNameProps) => { + const { + actions: { openRuleTranslationPreview: openRulePreview }, + } = useSiemMigrationsTableContext(); + + return ( + { + openRulePreview(ruleId); + }} + data-test-subj="ruleName" + > + {name} + + ); +}; + +export const RULE_NAME_COLUMN: TableColumn = { + 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 useSiemMigrationsTableColumns = (): TableColumn[] => { + return useMemo( + () => [ + RULE_NAME_COLUMN, + 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%', + }, + ], + [] + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/utils.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/utils.tsx new file mode 100644 index 0000000000000..c75fc775a6832 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/siem_migrations/utils.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/detection_engine/rule_management_ui/hooks/use_is_siem_migrations_enabled.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/hooks/use_is_siem_migrations_enabled.tsx new file mode 100644 index 0000000000000..8755df3210d29 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/hooks/use_is_siem_migrations_enabled.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 { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; + +export const useIsSiemMigrationsEnabled = () => { + return useIsExperimentalFeatureEnabled('siemMigrationsEnabled'); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/siem_migrations/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/siem_migrations/index.tsx new file mode 100644 index 0000000000000..4952953664716 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/siem_migrations/index.tsx @@ -0,0 +1,70 @@ +/* + * 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 { redirectToDetections } from '../../../../detections/pages/detection_engine/rules/helpers'; +import { SecurityPageName } from '../../../../app/types'; +import { HeaderPage } from '../../../../common/components/header_page'; +import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; +import { useKibana } from '../../../../common/lib/kibana'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; + +import { useUserData } from '../../../../detections/components/user_info'; +import { useListsConfig } from '../../../../detections/containers/detection_engine/lists/use_lists_config'; + +import * as i18n from './translations'; +import { SiemMigrationsTable } from '../../components/siem_migrations/siem_migrations_table'; +import { SiemMigrationsTableContextProvider } from '../../components/siem_migrations/siem_migrations_table_context'; +import { APP_UI_ID } from '../../../../../common'; +import { NeedAdminForUpdateRulesCallOut } from '../../../../detections/components/callouts/need_admin_for_update_callout'; +import { MissingPrivilegesCallOut } from '../../../../detections/components/callouts/missing_privileges_callout'; +import { getDetectionEngineUrl } from '../../../../common/components/link_to'; +import { SiemMigrationsHeaderButtons } from '../../components/siem_migrations/siem_migrations_header_buttons'; + +const SiemMigrationsPageComponent: React.FC = () => { + const { navigateToApp } = useKibana().services.application; + + const [{ isSignalIndexExists, isAuthenticated, hasEncryptionKey }] = useUserData(); + const { needsConfiguration: needsListsConfiguration } = useListsConfig(); + + if ( + redirectToDetections( + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + needsListsConfiguration + ) + ) { + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.alerts, + path: getDetectionEngineUrl(), + }); + return null; + } + + return ( + <> + + + + + + + + + + + + + + + ); +}; + +export const SiemMigrationsPage = React.memo(SiemMigrationsPageComponent); +SiemMigrationsPage.displayName = 'SiemMigrationsPage'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/siem_migrations/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/siem_migrations/translations.tsx new file mode 100644 index 0000000000000..1c5f8278fc4bc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/siem_migrations/translations.tsx @@ -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 PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.siemMigrations.pageTitle', + { + defaultMessage: 'Translated rules', + } +); diff --git a/x-pack/plugins/security_solution/public/rules/links.ts b/x-pack/plugins/security_solution/public/rules/links.ts index 5564a8b9b4e2a..122b4cd16efa1 100644 --- a/x-pack/plugins/security_solution/public/rules/links.ts +++ b/x-pack/plugins/security_solution/public/rules/links.ts @@ -13,6 +13,7 @@ import { RULES_CREATE_PATH, RULES_LANDING_PATH, RULES_PATH, + SIEM_MIGRATIONS_PATH, SERVER_APP_ID, } from '../../common/constants'; import { @@ -21,6 +22,7 @@ import { CREATE_NEW_RULE, EXCEPTIONS, RULES, + SIEM_MIGRATIONS, SIEM_RULES, } from '../app/translations'; import { SecurityPageName } from '../app/types'; @@ -106,6 +108,24 @@ export const links: LinkItem = { }), ], }, + { + id: SecurityPageName.rulesSiemMigrations, + title: SIEM_MIGRATIONS, + description: i18n.translate('xpack.securitySolution.appLinks.siemMigrationsDescription', { + defaultMessage: 'SIEM Rules Migrations.', + }), + landingIcon: IconConsoleCloud, + path: SIEM_MIGRATIONS_PATH, + capabilities: [`${SERVER_APP_ID}.show`], + skipUrlState: true, + hideTimeline: true, + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.siemMigrations', { + defaultMessage: 'SIEM Rules Migrations', + }), + ], + experimentalKey: 'siemMigrationsEnabled', + }, ], categories: [ { @@ -116,6 +136,7 @@ export const links: LinkItem = { SecurityPageName.rules, SecurityPageName.cloudSecurityPostureBenchmarks, SecurityPageName.exceptions, + SecurityPageName.rulesSiemMigrations, ], }, { diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index add8a5911df21..9a40509db45e3 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -30,38 +30,54 @@ import type { SecuritySubPluginRoutes } from '../app/types'; import { RulesLandingPage } from './landing'; import { CoverageOverviewPage } from '../detection_engine/rule_management_ui/pages/coverage_overview'; import { RuleDetailTabs } from '../detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs'; +import { SiemMigrationsPage } from '../detection_engine/rule_management_ui/pages/siem_migrations'; +import { useIsSiemMigrationsEnabled } from '../detection_engine/rule_management_ui/hooks/use_is_siem_migrations_enabled'; -const RulesSubRoutes = [ - { - path: '/rules/id/:detailName/edit', - main: EditRulePage, - exact: true, - }, - { - path: `/rules/id/:detailName/:tabName(${RuleDetailTabs.alerts}|${RuleDetailTabs.exceptions}|${RuleDetailTabs.endpointExceptions}|${RuleDetailTabs.executionResults}|${RuleDetailTabs.executionEvents})`, - main: RuleDetailsPage, - exact: true, - }, - { - path: '/rules/create', - main: CreateRulePage, - exact: true, - }, - { - path: `/rules/:tabName(${AllRulesTabs.management}|${AllRulesTabs.monitoring}|${AllRulesTabs.updates})`, - main: RulesPage, - exact: true, - }, - { - path: '/rules/add_rules', - main: AddRulesPage, - exact: true, - }, -]; +const getRulesSubRoutes = (isSiemMigrationsEnabled: boolean) => { + return [ + { + path: '/rules/id/:detailName/edit', + main: EditRulePage, + exact: true, + }, + { + path: `/rules/id/:detailName/:tabName(${RuleDetailTabs.alerts}|${RuleDetailTabs.exceptions}|${RuleDetailTabs.endpointExceptions}|${RuleDetailTabs.executionResults}|${RuleDetailTabs.executionEvents})`, + main: RuleDetailsPage, + exact: true, + }, + { + path: '/rules/create', + main: CreateRulePage, + exact: true, + }, + { + path: `/rules/:tabName(${AllRulesTabs.management}|${AllRulesTabs.monitoring}|${AllRulesTabs.updates})`, + main: RulesPage, + exact: true, + }, + { + path: '/rules/add_rules', + main: AddRulesPage, + exact: true, + }, + ...(isSiemMigrationsEnabled + ? [ + { + path: '/rules/siem_migrations', + main: SiemMigrationsPage, + exact: true, + }, + ] + : []), + ]; +}; const RulesContainerComponent: React.FC = () => { useReadonlyHeader(i18n.READ_ONLY_BADGE_TOOLTIP); + const isSiemMigrationsEnabled = useIsSiemMigrationsEnabled(); + const rulesSubRoutes = getRulesSubRoutes(isSiemMigrationsEnabled); + return ( @@ -87,7 +103,7 @@ const RulesContainerComponent: React.FC = () => { - {RulesSubRoutes.map((route) => ( + {rulesSubRoutes.map((route) => (