diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts index 531669608ed8b..88ed777c21d69 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts @@ -44,7 +44,7 @@ export enum SiemMigrationStatus { FAILED = 'failed', } -export enum SiemMigrationRuleTranslationResult { +export enum RuleTranslationResult { FULL = 'full', PARTIAL = 'partial', UNTRANSLATABLE = 'untranslatable', @@ -60,3 +60,5 @@ export const DEFAULT_TRANSLATION_FIELDS = { to: 'now', interval: '5m', } as const; + +export const EMPTY_RESOURCE_PLACEHOLDER = ''; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index d9c33ebbdf704..27db56aedb451 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -285,21 +285,47 @@ export const RuleMigrationTranslationStats = z.object({ */ rules: z.object({ /** - * The total number of rules to migrate. + * The total number of rules in the migration. */ total: z.number().int(), /** - * The number of rules that matched Elastic prebuilt rules. - */ - prebuilt: z.number().int(), - /** - * The number of rules that did not match Elastic prebuilt rules and will be installed as custom rules. + * The number of rules that have been successfully translated. */ - custom: z.number().int(), + success: z.object({ + /** + * The total number of rules that have been successfully translated. + */ + total: z.number().int(), + /** + * The translation results + */ + result: z.object({ + /** + * The number of rules that have been fully translated. + */ + full: z.number().int(), + /** + * The number of rules that have been partially translated. + */ + partial: z.number().int(), + /** + * The number of rules that could not be translated. + */ + untranslatable: z.number().int(), + }), + /** + * The number of rules that have been successfully translated and can be installed. + */ + installable: z.number().int(), + /** + * The number of rules that have been successfully translated and matched Elastic prebuilt rules. + */ + prebuilt: z.number().int(), + }), /** - * The number of rules that can be installed. + * The number of rules that have failed translation. */ - installable: z.number().int(), + failed: z.number().int(), }), }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 6fce9f0d51f5d..f3a85b3e37443 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -234,23 +234,50 @@ components: description: The rules migration translation stats. required: - total - - prebuilt - - custom - - installable + - success + - failed properties: total: type: integer - description: The total number of rules to migrate. - prebuilt: - type: integer - description: The number of rules that matched Elastic prebuilt rules. - custom: - type: integer - description: The number of rules that did not match Elastic prebuilt rules and will be installed as custom rules. - installable: + description: The total number of rules in the migration. + success: + type: object + description: The number of rules that have been successfully translated. + required: + - total + - result + - installable + - prebuilt + properties: + total: + type: integer + description: The total number of rules that have been successfully translated. + result: + type: object + description: The translation results + required: + - full + - partial + - untranslatable + properties: + full: + type: integer + description: The number of rules that have been fully translated. + partial: + type: integer + description: The number of rules that have been partially translated. + untranslatable: + type: integer + description: The number of rules that could not be translated. + installable: + type: integer + description: The number of rules that have been successfully translated and can be installed. + prebuilt: + type: integer + description: The number of rules that have been successfully translated and matched Elastic prebuilt rules. + failed: type: integer - description: The number of rules that can be installed. - + description: The number of rules that have failed translation. RuleMigrationTranslationResult: type: string description: The rule translation result. diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/index.ts new file mode 100644 index 0000000000000..e11d5f18e46be --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { PanelText, type PanelTextProps } from './panel_text'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/panel_text.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/panel_text.tsx new file mode 100644 index 0000000000000..5c1fc6746bcd0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/panel_text.tsx @@ -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 React, { type PropsWithChildren } from 'react'; +import { css, type CSSInterpolation } from '@emotion/css'; +import { EuiText, useEuiTheme, COLOR_MODES_STANDARD, type EuiTextProps } from '@elastic/eui'; + +export interface PanelTextProps extends PropsWithChildren { + subdued?: true; + semiBold?: true; +} +export const PanelText = React.memo(({ children, subdued, semiBold, ...props }) => { + const { euiTheme, colorMode } = useEuiTheme(); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + + let color; + if (subdued && !isDarkMode) { + color = 'subdued'; + } + + const style: CSSInterpolation = {}; + if (semiBold) { + style.fontWeight = euiTheme.font.weight.semiBold; + } + + return ( + + {children} + + ); +}); +PanelText.displayName = 'PanelText'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts index 387e9d66865b3..444aa7831802d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts @@ -28,7 +28,7 @@ const getCardHash = (cardId: OnboardingCardId | null) => (cardId ? `#${cardId}` * This hook manages the expanded card id state in the LocalStorage and the hash in the URL. */ export const useUrlDetail = () => { - const { spaceId, telemetry } = useOnboardingContext(); + const { config, spaceId, telemetry } = useOnboardingContext(); const topicId = useTopicId(); const [storedUrlDetail, setStoredUrlDetail] = useStoredUrlDetails(spaceId); @@ -56,6 +56,14 @@ export const useUrlDetail = () => { const syncUrlDetails = useCallback( (pathTopicId: OnboardingTopicId | null, hashCardId: OnboardingCardId | null) => { + if (storedUrlDetail) { + // If the stored topic is not valid, clear it + const [storedTopicId] = storedUrlDetail.split('#'); + if (storedTopicId && !config.has(storedTopicId as OnboardingTopicId)) { + setStoredUrlDetail(null); + return; + } + } const urlDetail = `${pathTopicId || ''}${hashCardId ? `#${hashCardId}` : ''}`; if (urlDetail && urlDetail !== storedUrlDetail) { if (hashCardId) { @@ -67,7 +75,7 @@ export const useUrlDetail = () => { navigateTo({ deepLinkId: SecurityPageName.landing, path: storedUrlDetail }); } }, - [navigateTo, setStoredUrlDetail, storedUrlDetail, telemetry] + [config, navigateTo, setStoredUrlDetail, storedUrlDetail, telemetry] ); return { setTopicDetail, setCardDetail, syncUrlDetails }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index e42834e85d488..1786c9cbee85c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx @@ -7,6 +7,7 @@ import React, { useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner'; import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; import { useDefinedLocalStorage } from '../../../../hooks/use_stored_state'; import type { OnboardingCardComponent } from '../../../../../types'; @@ -35,9 +36,15 @@ export const AIConnectorCard: OnboardingCardComponent = [setComplete, setStoredConnectorId] ); - const connectors = checkCompleteMetadata?.connectors; - const canExecuteConnectors = checkCompleteMetadata?.canExecuteConnectors; - const canCreateConnectors = checkCompleteMetadata?.canCreateConnectors; + if (!checkCompleteMetadata) { + return ( + + + + ); + } + + const { connectors, canExecuteConnectors, canCreateConnectors } = checkCompleteMetadata; return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx deleted file mode 100644 index 49baaba65caca..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { createContext, useContext, useMemo, type PropsWithChildren } from 'react'; -import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; - -interface StartMigrationContextValue { - openFlyout: (migrationStats?: RuleMigrationTaskStats) => void; - closeFlyout: () => void; -} - -const StartMigrationContext = createContext(null); - -export const StartMigrationContextProvider: React.FC< - PropsWithChildren -> = React.memo(({ children, openFlyout, closeFlyout }) => { - const value = useMemo( - () => ({ openFlyout, closeFlyout }), - [openFlyout, closeFlyout] - ); - return {children}; -}); -StartMigrationContextProvider.displayName = 'StartMigrationContextProvider'; - -export const useStartMigrationContext = (): StartMigrationContextValue => { - const context = useContext(StartMigrationContext); - if (context == null) { - throw new Error('useStartMigrationContext must be used within a StartMigrationContextProvider'); - } - return context; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx deleted file mode 100644 index 0527e1cfbdf17..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiPanel, EuiProgress } from '@elastic/eui'; -import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; -import * as i18n from '../translations'; -import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; - -export interface MigrationProgressPanelProps { - migrationStats: RuleMigrationStats; -} -export const MigrationProgressPanel = React.memo( - ({ migrationStats }) => { - const progressValue = useMemo(() => { - const finished = migrationStats.rules.completed + migrationStats.rules.failed; - return (finished / migrationStats.rules.total) * 100; - }, [migrationStats.rules]); - - return ( - - - - -

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

-
-
- - -

{i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION}

-
-
- - - -
-
- ); - } -); -MigrationProgressPanel.displayName = 'MigrationProgressPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx deleted file mode 100644 index 8603511fa2d6f..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiButton, - EuiButtonEmpty, - EuiPanel, -} from '@elastic/eui'; -import { useStartMigration } from '../../../../../../../siem_migrations/rules/service/hooks/use_start_migration'; -import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; -import * as i18n from '../translations'; -import { useStartMigrationContext } from '../context'; -import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; - -export interface MigrationReadyPanelProps { - migrationStats: RuleMigrationStats; -} -export const MigrationReadyPanel = React.memo(({ migrationStats }) => { - const { openFlyout } = useStartMigrationContext(); - const onOpenFlyout = useCallback(() => { - openFlyout(migrationStats); - }, [openFlyout, migrationStats]); - - const { startMigration, isLoading } = useStartMigration(); - const onStartMigration = useCallback(() => { - startMigration(migrationStats.id); - }, [migrationStats.id, startMigration]); - - return ( - - - - - - -

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

-
-
- - -

{i18n.START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION}

-
-
-
-
- - - {i18n.START_MIGRATION_CARD_TRANSLATE_BUTTON} - - - - - {i18n.START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON} - - -
-
- ); -}); -MigrationReadyPanel.displayName = 'MigrationReadyPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx deleted file mode 100644 index b73b3cc8b4921..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import moment from 'moment'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiPanel, - EuiHorizontalRule, - EuiIcon, -} from '@elastic/eui'; -import { SecurityPageName } from '@kbn/security-solution-navigation'; -import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; -import { SecuritySolutionLinkButton } from '../../../../../../../common/components/links'; -import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; -import * as i18n from '../translations'; -import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; - -export interface MigrationResultPanelProps { - migrationStats: RuleMigrationStats; -} -export const MigrationResultPanel = React.memo(({ migrationStats }) => { - return ( - - - - - -

{i18n.START_MIGRATION_CARD_RESULT_TITLE(migrationStats.number)}

-
-
- - -

- {i18n.START_MIGRATION_CARD_RESULT_DESCRIPTION( - moment(migrationStats.created_at).format('MMMM Do YYYY, h:mm:ss a'), - moment(migrationStats.last_updated_at).fromNow() - )} -

-
-
-
-
- - - - - - - - - - -

{i18n.VIEW_TRANSLATED_RULES_TITLE}

-
-
-
-
- - - - -

{'TODO: chart'}

-
- - - {i18n.VIEW_TRANSLATED_RULES_BUTTON} - - -
-
-
-
-
-
- ); -}); -MigrationResultPanel.displayName = 'MigrationResultPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx new file mode 100644 index 0000000000000..1dae4d523c953 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; +import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types'; +import { UploadRulesPanel } from './upload_rules_panel'; +import { MigrationProgressPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_progress_panel'; +import { MigrationResultPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_result_panel'; +import { MigrationReadyPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_ready_panel'; +import { MissingAIConnectorCallout } from './missing_ai_connector_callout'; + +export interface RuleMigrationsPanelsProps { + migrationsStats: RuleMigrationStats[]; + isConnectorsCardComplete: boolean; + expandConnectorsCard: () => void; +} +export const RuleMigrationsPanels = React.memo( + ({ migrationsStats, isConnectorsCardComplete, expandConnectorsCard }) => { + if (migrationsStats.length === 0) { + return isConnectorsCardComplete ? ( + + ) : ( + + ); + } + + return ( + + + {isConnectorsCardComplete ? ( + + ) : ( + + )} + + {migrationsStats.map((migrationStats) => ( + + {migrationStats.status === SiemMigrationTaskStatus.READY && ( + + )} + {migrationStats.status === SiemMigrationTaskStatus.RUNNING && ( + + )} + {migrationStats.status === SiemMigrationTaskStatus.FINISHED && ( + + )} + + ))} + + ); + } +); +RuleMigrationsPanels.displayName = 'RuleMigrationsPanels'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index a8d7aa78d0c93..baebbde53b4cf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -5,32 +5,25 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; -import { EuiSpacer, EuiText } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { PanelText } from '../../../../../../common/components/panel_text'; +import { RuleMigrationDataInputWrapper } from '../../../../../../siem_migrations/rules/components/data_input_flyout/data_input_wrapper'; import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; import { OnboardingCardId } from '../../../../../constants'; -import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; import { useLatestStats } from '../../../../../../siem_migrations/rules/service/hooks/use_latest_stats'; -import { MigrationDataInputFlyout } from '../../../../../../siem_migrations/rules/components/data_input_flyout'; import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner'; import type { OnboardingCardComponent } from '../../../../../types'; import { OnboardingCardContentPanel } from '../../common/card_content_panel'; -import { UploadRulesPanels } from './upload_rules_panels'; -import { StartMigrationContextProvider } from './context'; +import { RuleMigrationsPanels } from './rule_migrations_panels'; import { useStyles } from './start_migration_card.styles'; import * as i18n from './translations'; -import { MissingAIConnectorCallout } from './missing_ai_connector_callout'; export const StartMigrationCard: OnboardingCardComponent = React.memo( ({ setComplete, isCardComplete, setExpandedCardId }) => { const styles = useStyles(); const { data: migrationsStats, isLoading, refreshStats } = useLatestStats(); - const [isFlyoutOpen, setIsFlyoutOpen] = useState(); - const [flyoutMigrationStats, setFlyoutMigrationStats] = useState< - RuleMigrationTaskStats | undefined - >(); - useEffect(() => { // Set card complete if any migration is finished if (!isCardComplete(OnboardingCardId.siemMigrationsStart) && migrationsStats) { @@ -40,44 +33,33 @@ export const StartMigrationCard: OnboardingCardComponent = React.memo( } }, [isCardComplete, migrationsStats, setComplete]); - const closeFlyout = useCallback(() => { - setIsFlyoutOpen(false); - setFlyoutMigrationStats(undefined); - refreshStats(); - }, [refreshStats]); - - const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => { - setFlyoutMigrationStats(migrationStats); - setIsFlyoutOpen(true); - }, []); + const isConnectorsCardComplete = useMemo( + () => isCardComplete(OnboardingCardId.siemMigrationsAiConnectors), + [isCardComplete] + ); - if (!isCardComplete(OnboardingCardId.siemMigrationsAiConnectors)) { - return ( - - setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors) - } - /> - ); - } + const expandConnectorsCard = useCallback(() => { + setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors); + }, [setExpandedCardId]); return ( - + {isLoading ? ( ) : ( - + )} - +

{i18n.START_MIGRATION_CARD_FOOTER_NOTE}

-
+
- {isFlyoutOpen && ( - - )} -
+ ); } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index bdb3f31842549..4073423f1f8ae 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -55,64 +55,3 @@ export const START_MIGRATION_CARD_UPLOAD_MORE_BUTTON = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.uploadMore.button', { defaultMessage: 'Upload more rules' } ); - -export const START_MIGRATION_CARD_UPLOAD_READ_MORE = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.upload.readMore', - { defaultMessage: 'Read more about our AI powered translations and other features.' } -); - -export const START_MIGRATION_CARD_UPLOAD_READ_DOCS = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.upload.readAiDocsLink', - { defaultMessage: 'Read AI docs' } -); - -export const START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.ready.description', - { - defaultMessage: - 'Migration is created and ready but the translation has not started yet. You can either upload macros & lookups or start the translation process', - } -); -export const START_MIGRATION_CARD_TRANSLATE_BUTTON = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.translate.button', - { defaultMessage: 'Start translation' } -); -export const START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.uploadMacros.button', - { defaultMessage: 'Upload macros' } -); - -export const START_MIGRATION_CARD_MIGRATION_TITLE = (number: number) => - i18n.translate('xpack.securitySolution.onboarding.startMigration.migrationTitle', { - defaultMessage: 'SIEM rules migration #{number}', - values: { number }, - }); - -export const START_MIGRATION_CARD_PROGRESS_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.progress.description', - { - defaultMessage: `This may take a few minutes & the task will work in the background. Just stay logged in and we'll notify you when done.`, - } -); - -export const START_MIGRATION_CARD_RESULT_TITLE = (number: number) => - i18n.translate('xpack.securitySolution.onboarding.startMigration.result.title', { - defaultMessage: 'SIEM rules migration #{number} complete', - values: { number }, - }); - -export const START_MIGRATION_CARD_RESULT_DESCRIPTION = (createdAt: string, finishedAt: string) => - i18n.translate('xpack.securitySolution.onboarding.startMigration.result.description', { - defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.', - values: { createdAt, finishedAt }, - }); - -export const VIEW_TRANSLATED_RULES_TITLE = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.result.translatedRules.title', - { defaultMessage: 'Translation Summary' } -); - -export const VIEW_TRANSLATED_RULES_BUTTON = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.result.translatedRules.button', - { defaultMessage: 'View translated rules' } -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx similarity index 82% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx index edcff3646c5aa..1a9bd2d17b945 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx @@ -15,10 +15,11 @@ import { EuiButtonEmpty, EuiPanel, } from '@elastic/eui'; -import { SiemMigrationsIcon } from '../../../../../../../siem_migrations/common/icon'; -import * as i18n from '../translations'; -import { useStartMigrationContext } from '../context'; -import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; +import { RuleMigrationsReadMore } from '../../../../../../siem_migrations/rules/components/migration_status_panels/read_more'; +import { SiemMigrationsIcon } from '../../../../../../siem_migrations/common/icon'; +import * as i18n from './translations'; +import { TITLE_CLASS_NAME } from './start_migration_card.styles'; +import { useRuleMigrationDataInputContext } from '../../../../../../siem_migrations/rules/components/data_input_flyout/context'; import { useStyles } from './upload_rules_panel.styles'; export interface UploadRulesPanelProps { @@ -26,7 +27,7 @@ export interface UploadRulesPanelProps { } export const UploadRulesPanel = React.memo(({ isUploadMore = false }) => { const styles = useStyles(isUploadMore); - const { openFlyout } = useStartMigrationContext(); + const { openFlyout } = useRuleMigrationDataInputContext(); const onOpenFlyout = useCallback(() => { openFlyout(); }, [openFlyout]); @@ -55,9 +56,7 @@ export const UploadRulesPanel = React.memo(({ isUploadMor - -

{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}

-
+
)} diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx deleted file mode 100644 index 6d011fc5fbb5b..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; -import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types'; -import { UploadRulesPanel } from './panels/upload_rules_panel'; -import { MigrationProgressPanel } from './panels/migration_progress_panel'; -import { MigrationResultPanel } from './panels/migration_result_panel'; -import { MigrationReadyPanel } from './panels/migration_ready_panel'; - -export interface UploadRulesPanelsProps { - migrationsStats: RuleMigrationStats[]; -} -export const UploadRulesPanels = React.memo(({ migrationsStats }) => { - if (migrationsStats.length === 0) { - return ; - } - - return ( - - - - - {migrationsStats.map((migrationStats) => ( - - {migrationStats.status === SiemMigrationTaskStatus.READY && ( - - )} - {migrationStats.status === SiemMigrationTaskStatus.RUNNING && ( - - )} - {migrationStats.status === SiemMigrationTaskStatus.FINISHED && ( - - )} - - ))} - - ); -}); -UploadRulesPanels.displayName = 'UploadRulesPanels'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/context.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/context.tsx new file mode 100644 index 0000000000000..bc37df49415c0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/context.tsx @@ -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 React, { createContext, useContext, useMemo, type PropsWithChildren } from 'react'; +import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; + +interface RuleMigrationDataInputContextValue { + openFlyout: (migrationStats?: RuleMigrationTaskStats) => void; + closeFlyout: () => void; +} + +const RuleMigrationDataInputContext = createContext( + null +); + +export const RuleMigrationDataInputContextProvider: React.FC< + PropsWithChildren +> = React.memo(({ children, openFlyout, closeFlyout }) => { + const value = useMemo( + () => ({ openFlyout, closeFlyout }), + [openFlyout, closeFlyout] + ); + return ( + + {children} + + ); +}); +RuleMigrationDataInputContextProvider.displayName = 'RuleMigrationDataInputContextProvider'; + +export const useRuleMigrationDataInputContext = (): RuleMigrationDataInputContextValue => { + const context = useContext(RuleMigrationDataInputContext); + if (context == null) { + throw new Error( + 'useRuleMigrationDataInputContext must be used within a RuleMigrationDataInputContextProvider' + ); + } + return context; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index ffc40c59d495a..9062e3a6b21e8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -15,6 +15,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButton, + EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { @@ -23,8 +24,9 @@ import type { } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { RulesDataInput } from './steps/rules/rules_data_input'; import { useStartMigration } from '../../service/hooks/use_start_migration'; -import { DataInputStep } from './types'; +import { DataInputStep } from './steps/constants'; import { MacrosDataInput } from './steps/macros/macros_data_input'; +import { LookupsDataInput } from './steps/lookups/lookups_data_input'; interface MissingResourcesIndexed { macros: string[]; @@ -84,8 +86,8 @@ export const MigrationDataInputFlyout = React.memo { - setDataInputStep(DataInputStep.Lookups); + const onAllLookupsCreated = useCallback(() => { + setDataInputStep(DataInputStep.End); }, []); return ( @@ -121,21 +123,28 @@ export const MigrationDataInputFlyout = React.memo + + + - + - + void; +} +export const RuleMigrationDataInputWrapper = React.memo< + PropsWithChildren +>(({ children, onFlyoutClosed }) => { + const [isFlyoutOpen, setIsFlyoutOpen] = useState(); + const [flyoutMigrationStats, setFlyoutMigrationStats] = useState< + RuleMigrationTaskStats | undefined + >(); + + const closeFlyout = useCallback(() => { + setIsFlyoutOpen(false); + setFlyoutMigrationStats(undefined); + onFlyoutClosed?.(); + }, [onFlyoutClosed]); + + const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => { + setFlyoutMigrationStats(migrationStats); + setIsFlyoutOpen(true); + }, []); + + return ( + + {children} + {isFlyoutOpen && ( + + )} + + ); +}); +RuleMigrationDataInputWrapper.displayName = 'RuleMigrationDataInputWrapper'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step.tsx similarity index 67% rename from x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx rename to x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step.tsx index fc0bd0e8c3b44..bb76ea749e15a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { EuiPanel } from '@elastic/eui'; +import { EuiPanel, EuiSteps, type EuiStepProps } from '@elastic/eui'; import { css } from '@emotion/css'; -import type { PropsWithChildren } from 'react'; import React from 'react'; const style = css` @@ -16,11 +15,11 @@ const style = css` } `; -export const SubStepsWrapper = React.memo>(({ children }) => { +export const SubSteps = React.memo<{ steps: EuiStepProps[] }>(({ steps }) => { return ( - {children} + ); }); -SubStepsWrapper.displayName = 'SubStepsWrapper'; +SubSteps.displayName = 'SubSteps'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts index b99cf826194f9..54622191b6d68 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts @@ -26,7 +26,7 @@ export const useParseFileInput = (onFileParsed: OnFileParsed) => { setError(undefined); - const rulesFile = files[0]; + const file = files[0]; const reader = new FileReader(); reader.onloadstart = () => setIsParsing(true); @@ -68,7 +68,7 @@ export const useParseFileInput = (onFileParsed: OnFileParsed) => { reader.onerror = handleReaderError; reader.onabort = handleReaderError; - reader.readAsText(rulesFile); + reader.readAsText(file); }, [onFileParsed] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/constants.ts new file mode 100644 index 0000000000000..c0586108b0a19 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/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. + */ + +export enum DataInputStep { + Rules = 1, + Macros = 2, + Lookups = 3, + End = 10, +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx new file mode 100644 index 0000000000000..a8fca750ce5da --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx @@ -0,0 +1,143 @@ +/* + * 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 { EuiStepProps } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiStepNumber, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../common/siem_migrations/constants'; +import type { + RuleMigrationResourceData, + RuleMigrationTaskStats, +} from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { OnResourcesCreated } from '../../types'; +import { getStatus } from '../common/get_status'; +import * as i18n from './translations'; +import { DataInputStep } from '../constants'; +import { SubSteps } from '../common/sub_step'; +import { useMissingLookupsListStep } from './sub_steps/missing_lookups_list'; +import { useLookupsFileUploadStep } from './sub_steps/lookups_file_upload'; + +export type UploadedLookups = Record; +export type AddUploadedLookups = (lookups: RuleMigrationResourceData[]) => void; + +interface LookupsDataInputSubStepsProps { + migrationStats: RuleMigrationTaskStats; + missingLookups: string[]; + onAllLookupsCreated: OnResourcesCreated; +} +interface LookupsDataInputProps + extends Omit { + dataInputStep: DataInputStep; + migrationStats?: RuleMigrationTaskStats; + missingLookups?: string[]; +} +export const LookupsDataInput = React.memo( + ({ dataInputStep, migrationStats, missingLookups, onAllLookupsCreated }) => { + const dataInputStatus = useMemo( + () => getStatus(DataInputStep.Lookups, dataInputStep), + [dataInputStep] + ); + + return ( + + + + + + + + + + {i18n.LOOKUPS_DATA_INPUT_TITLE} + + + + + {dataInputStatus === 'current' && migrationStats && missingLookups && ( + <> + + + {i18n.LOOKUPS_DATA_INPUT_DESCRIPTION} + + + + + + + )} + + + ); + } +); +LookupsDataInput.displayName = 'LookupsDataInput'; + +const END = 10 as const; +type SubStep = 1 | 2 | typeof END; +export const LookupsDataInputSubSteps = React.memo( + ({ migrationStats, missingLookups, onAllLookupsCreated }) => { + const [subStep, setSubStep] = useState(1); + const [uploadedLookups, setUploadedLookups] = useState({}); + + const addUploadedLookups = useCallback((lookups) => { + setUploadedLookups((prevUploadedLookups) => ({ + ...prevUploadedLookups, + ...Object.fromEntries( + lookups.map((lookup) => [lookup.name, lookup.content ?? EMPTY_RESOURCE_PLACEHOLDER]) + ), + })); + }, []); + + useEffect(() => { + if (missingLookups.every((lookupName) => uploadedLookups[lookupName])) { + setSubStep(END); + onAllLookupsCreated(); + } + }, [uploadedLookups, missingLookups, onAllLookupsCreated]); + + // Copy query step + const onCopied = useCallback(() => { + setSubStep(2); + }, []); + const copyStep = useMissingLookupsListStep({ + status: getStatus(1, subStep), + migrationStats, + missingLookups, + uploadedLookups, + addUploadedLookups, + onCopied, + }); + + // Upload macros step + const uploadStep = useLookupsFileUploadStep({ + status: getStatus(2, subStep), + migrationStats, + missingLookups, + addUploadedLookups, + }); + + const steps = useMemo(() => [copyStep, uploadStep], [copyStep, uploadStep]); + + return ; + } +); +LookupsDataInputSubSteps.displayName = 'LookupsDataInputActive'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx new file mode 100644 index 0000000000000..f15413768b9a7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.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 React, { useCallback, useMemo } from 'react'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources'; +import type { + RuleMigrationResourceData, + RuleMigrationTaskStats, +} from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { AddUploadedLookups } from '../../lookups_data_input'; +import * as i18n from './translations'; +import { LookupsFileUpload } from './lookups_file_upload'; + +export interface RulesFileUploadStepProps { + status: EuiStepStatus; + migrationStats: RuleMigrationTaskStats; + missingLookups: string[]; + addUploadedLookups: AddUploadedLookups; +} +export const useLookupsFileUploadStep = ({ + status, + migrationStats, + addUploadedLookups, +}: RulesFileUploadStepProps): EuiStepProps => { + const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups); + + const upsertMigrationResources = useCallback( + (lookupsFromFile: RuleMigrationResourceData[]) => { + if (lookupsFromFile.length === 0) { + return; // No lookups provided + } + upsertResources(migrationStats.id, lookupsFromFile); + }, + [upsertResources, migrationStats] + ); + + const uploadStepStatus = useMemo(() => { + if (isLoading) { + return 'loading'; + } + if (error) { + return 'danger'; + } + return status; + }, [isLoading, error, status]); + + return { + title: i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_TITLE, + status: uploadStepStatus, + children: ( + + ), + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx new file mode 100644 index 0000000000000..6ea9562f24cce --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx @@ -0,0 +1,163 @@ +/* + * 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, useMemo, useRef, useState } from 'react'; +import { + EuiButton, + EuiFilePicker, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiText, +} from '@elastic/eui'; +import type { + EuiFilePickerClass, + EuiFilePickerProps, +} from '@elastic/eui/src/components/form/file_picker/file_picker'; +import type { RuleMigrationResourceData } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { FILE_UPLOAD_ERROR } from '../../../../translations'; +import * as i18n from './translations'; + +export interface LookupsFileUploadProps { + createResources: (resources: RuleMigrationResourceData[]) => void; + apiError?: string; + isLoading?: boolean; +} +export const LookupsFileUpload = React.memo( + ({ createResources, apiError, isLoading }) => { + const [lookupResources, setLookupResources] = useState([]); + const filePickerRef = useRef(null); + + const createLookups = useCallback(() => { + filePickerRef.current?.removeFiles(); + createResources(lookupResources); + }, [createResources, lookupResources]); + + const [isParsing, setIsParsing] = useState(false); + const [fileErrors, setErrors] = useState([]); + const addError = useCallback((error: string) => { + setErrors((current) => [...current, error]); + }, []); + + const parseFile = useCallback( + async (files: FileList | null) => { + setErrors([]); + setLookupResources([]); + + if (!files?.length) { + return; + } + + const lookups = await Promise.all( + Array.from(files).map((file) => { + return new Promise((resolve) => { + const reader = new FileReader(); + + reader.onloadstart = () => setIsParsing(true); + reader.onloadend = () => setIsParsing(false); + + reader.onload = function (e) { + // We can safely cast to string since we call `readAsText` to load the file. + const content = e.target?.result as string | undefined; + + if (content == null) { + addError(FILE_UPLOAD_ERROR.CAN_NOT_READ); + return; + } + + if (content === '' && e.loaded > 100000) { + // V8-based browsers can't handle large files and return an empty string + // instead of an error; see https://stackoverflow.com/a/61316641 + addError(FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + return; + } + + const name = file.name.replace(/\.[^/.]+$/, '').trim(); + resolve({ type: 'list', name, content }); + }; + + const handleReaderError = function () { + const message = reader.error?.message; + if (message) { + addError(FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); + } else { + addError(FILE_UPLOAD_ERROR.CAN_NOT_READ); + } + }; + + reader.onerror = handleReaderError; + reader.onabort = handleReaderError; + + reader.readAsText(file); + }); + }) + ).catch((e) => { + addError(e.message); + return []; + }); + // Set the loaded lookups to the state + setLookupResources((current) => [...current, ...lookups]); + }, + [addError] + ); + + const errors = useMemo(() => { + if (apiError) { + return [apiError]; + } + return fileErrors; + }, [apiError, fileErrors]); + + return ( + + + ( + + {error} + + ))} + isInvalid={errors.length > 0} + fullWidth + > + >} + fullWidth + initialPromptText={ + <> + + {i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_PROMPT} + + + } + accept="application/text" + onChange={parseFile} + multiple + display="large" + aria-label="Upload lookups files" + isLoading={isParsing || isLoading} + disabled={isParsing || isLoading} + data-test-subj="lookupsFilePicker" + data-loading={isParsing} + /> + + + + + + + {i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_BUTTON} + + + + + + ); + } +); +LookupsFileUpload.displayName = 'LookupsFileUpload'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts new file mode 100644 index 0000000000000..492f51309ca53 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts @@ -0,0 +1,34 @@ +/* + * 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 LOOKUPS_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.title', + { defaultMessage: 'Update your lookups export' } +); +export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.prompt', + { defaultMessage: 'Select or drag and drop the exported lookup files' } +); +export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_NOT_UPLOADED_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.notUploadedTitle', + { defaultMessage: 'Lookups not uploaded' } +); +export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_NOT_UPLOADED = (lookupsNames: string) => + i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.notUploaded', + { + defaultMessage: 'The following files did not match any missing lookup: {lookupsNames}', + values: { lookupsNames }, + } + ); + +export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.button', + { defaultMessage: 'Upload' } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx new file mode 100644 index 0000000000000..ae1dbc0a03b3c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx @@ -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 React, { useCallback, useMemo } from 'react'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../../../common/siem_migrations/constants'; +import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources'; +import type { RuleMigrationTaskStats } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { UploadedLookups, AddUploadedLookups } from '../../lookups_data_input'; +import * as i18n from './translations'; +import { MissingLookupsList } from './missing_lookups_list'; + +export interface MissingLookupsListStepProps { + status: EuiStepStatus; + migrationStats: RuleMigrationTaskStats; + missingLookups: string[]; + uploadedLookups: UploadedLookups; + addUploadedLookups: AddUploadedLookups; + onCopied: () => void; +} +export const useMissingLookupsListStep = ({ + status, + migrationStats, + missingLookups, + uploadedLookups, + addUploadedLookups, + onCopied, +}: MissingLookupsListStepProps): EuiStepProps => { + const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups); + + const clearLookup = useCallback( + (lookupName: string) => { + upsertResources(migrationStats.id, [ + { type: 'list', name: lookupName, content: EMPTY_RESOURCE_PLACEHOLDER }, + ]); + }, + [upsertResources, migrationStats] + ); + + const listStepStatus = useMemo(() => { + if (isLoading) { + return 'loading'; + } + if (error) { + return 'danger'; + } + return status; + }, [isLoading, error, status]); + + return { + title: i18n.LOOKUPS_DATA_INPUT_COPY_TITLE, + status: listStepStatus, + children: ( + + ), + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/missing_lookups_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/missing_lookups_list.tsx new file mode 100644 index 0000000000000..cd462a41bb6c1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/missing_lookups_list.tsx @@ -0,0 +1,161 @@ +/* + * 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, useMemo, useState } from 'react'; +import { css } from '@emotion/css'; +import { + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../../../common/siem_migrations/constants'; +import type { UploadedLookups } from '../../lookups_data_input'; +import * as i18n from './translations'; + +const scrollPanelCss = css` + max-height: 200px; + overflow-y: auto; +`; + +interface MissingLookupsListProps { + missingLookups: string[]; + uploadedLookups: UploadedLookups; + clearLookup: (lookupsName: string) => void; + onCopied: () => void; +} +export const MissingLookupsList = React.memo( + ({ missingLookups, uploadedLookups, clearLookup, onCopied }) => { + const { euiTheme } = useEuiTheme(); + return ( + <> + + + {missingLookups.map((lookupName) => { + const isMarkedAsEmpty = uploadedLookups[lookupName] === EMPTY_RESOURCE_PLACEHOLDER; + return ( + + + + {uploadedLookups[lookupName] ? ( + + ) : ( + + )} + + + + {lookupName} + + + + + {(copy) => ( + + )} + + + + + + + + ); + })} + + + + + {i18n.MISSING_LOOKUPS_DESCRIPTION} + + + ); + } +); +MissingLookupsList.displayName = 'MissingLookupsList'; + +interface CopyLookupNameButtonProps { + lookupName: string; + onCopied: () => void; + copy: () => void; +} +const CopyLookupNameButton = React.memo( + ({ lookupName, onCopied, copy }) => { + const onClick = useCallback(() => { + copy(); + onCopied(); + }, [copy, onCopied]); + return ( + + + + ); + } +); +CopyLookupNameButton.displayName = 'CopyLookupNameButton'; + +interface ClearLookupButtonProps { + lookupName: string; + clearLookup: (lookupName: string) => void; + isDisabled: boolean; +} +const ClearLookupButton = React.memo( + ({ lookupName, clearLookup, isDisabled: isDisabledDefault }) => { + const [isDisabled, setIsDisabled] = useState(isDisabledDefault); + const onClick = useCallback(() => { + setIsDisabled(true); + clearLookup(lookupName); + }, [clearLookup, lookupName]); + + const button = useMemo( + () => ( + + ), + [onClick, isDisabled] + ); + if (isDisabled) { + return button; + } + return {button}; + } +); +ClearLookupButton.displayName = 'ClearLookupButton'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts new file mode 100644 index 0000000000000..123e541182068 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.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 { i18n } from '@kbn/i18n'; + +export const LOOKUPS_DATA_INPUT_COPY_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.title', + { defaultMessage: 'Lookups found in your rules' } +); + +export const MISSING_LOOKUPS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.description', + { + defaultMessage: + 'For your lookups, go to your admin Splunk account and the Search and Reporting app Lookups page. Download the following lookups individually and upload below.', + } +); + +export const COPY_LOOKUP_NAME_TOOLTIP = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.copyLookupNameTooltip', + { defaultMessage: 'Copy lookup name' } +); +export const CLEAR_EMPTY_LOOKUP_TOOLTIP = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.clearEmptyLookupTooltip', + { defaultMessage: 'Mark the lookup as empty' } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/translations.ts new file mode 100644 index 0000000000000..970bff4785c82 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const LOOKUPS_DATA_INPUT_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.title', + { defaultMessage: 'Upload identified lookups' } +); +export const LOOKUPS_DATA_INPUT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.description', + { + defaultMessage: `We've also found lookups within your rules. To fully translate those rules containing these lookups, follow the step-by-step guide to export and upload them all.`, + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx index f19e704b96710..ebbffb6d7f6d6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx @@ -6,30 +6,21 @@ */ import type { EuiStepProps } from '@elastic/eui'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiStepNumber, - EuiSteps, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStepNumber, EuiTitle } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; -import { SubStepsWrapper } from '../common/sub_step_wrapper'; -import type { OnResourcesCreated, OnMissingResourcesFetched, DataInputStep } from '../../types'; +import type { OnResourcesCreated, OnMissingResourcesFetched } from '../../types'; import { getStatus } from '../common/get_status'; +import * as i18n from './translations'; +import { DataInputStep } from '../constants'; +import { SubSteps } from '../common/sub_step'; import { useCopyExportQueryStep } from './sub_steps/copy_export_query'; import { useMacrosFileUploadStep } from './sub_steps/macros_file_upload'; -import * as i18n from './translations'; import { useCheckResourcesStep } from './sub_steps/check_resources'; -const DataInputStepNumber: DataInputStep = 2; - interface MacrosDataInputSubStepsProps { migrationStats: RuleMigrationTaskStats; missingMacros: string[]; - onMacrosCreated: OnResourcesCreated; onMissingResourcesFetched: OnMissingResourcesFetched; } interface MacrosDataInputProps @@ -39,15 +30,9 @@ interface MacrosDataInputProps missingMacros?: string[]; } export const MacrosDataInput = React.memo( - ({ - dataInputStep, - migrationStats, - missingMacros, - onMacrosCreated, - onMissingResourcesFetched, - }) => { + ({ dataInputStep, migrationStats, missingMacros, onMissingResourcesFetched }) => { const dataInputStatus = useMemo( - () => getStatus(DataInputStepNumber, dataInputStep), + () => getStatus(DataInputStep.Macros, dataInputStep), [dataInputStep] ); @@ -59,7 +44,7 @@ export const MacrosDataInput = React.memo( @@ -75,7 +60,6 @@ export const MacrosDataInput = React.memo( @@ -90,7 +74,7 @@ MacrosDataInput.displayName = 'MacrosDataInput'; const END = 10 as const; type SubStep = 1 | 2 | 3 | typeof END; export const MacrosDataInputSubSteps = React.memo( - ({ migrationStats, missingMacros, onMacrosCreated, onMissingResourcesFetched }) => { + ({ migrationStats, missingMacros, onMissingResourcesFetched }) => { const [subStep, setSubStep] = useState(missingMacros.length ? 1 : 3); // Copy query step @@ -101,9 +85,8 @@ export const MacrosDataInputSubSteps = React.memo( // Upload macros step const onMacrosCreatedStep = useCallback(() => { - onMacrosCreated(); setSubStep(3); - }, [onMacrosCreated]); + }, []); const uploadStep = useMacrosFileUploadStep({ status: getStatus(2, subStep), migrationStats, @@ -130,11 +113,7 @@ export const MacrosDataInputSubSteps = React.memo( [copyStep, uploadStep, resourcesStep] ); - return ( - - - - ); + return ; } ); MacrosDataInputSubSteps.displayName = 'MacrosDataInputActive'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/copy_export_query.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/copy_export_query.tsx index 93f2ce715184c..9988be28ee847 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/copy_export_query.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/copy_export_query.tsx @@ -42,7 +42,7 @@ export const CopyExportQuery = React.memo(({ onCopied }) = id="xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.description" defaultMessage="From you admin Splunk account, go to the {section} app and run the above query. Export your results as {format}." values={{ - section: {i18n.RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION}, + section: {i18n.MACROS_DATA_INPUT_COPY_DESCRIPTION_SECTION}, format: {'JSON'}, }} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx index 3d2adcc78857b..ac8ff1521c5af 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx @@ -19,7 +19,7 @@ export const useCopyExportQueryStep = ({ onCopied, }: CopyExportQueryStepProps): EuiStepProps => { return { - title: i18n.RULES_DATA_INPUT_COPY_TITLE, + title: i18n.MACROS_DATA_INPUT_COPY_TITLE, status, children: , }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts index 71466a54dd138..89364dbfefd3b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; -export const RULES_DATA_INPUT_COPY_TITLE = i18n.translate( +export const MACROS_DATA_INPUT_COPY_TITLE = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.copyExportQuery.title', { defaultMessage: 'Copy macros query' } ); -export const RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate( +export const MACROS_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.copyExportQuery.description.section', { defaultMessage: 'Search and Reporting' } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx index f2353e3f0276a..3906ac6ca8a09 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx @@ -81,7 +81,7 @@ export const useMacrosFileUploadStep = ({ }, [isLoading, error, status]); return { - title: i18n.RULES_DATA_INPUT_FILE_UPLOAD_TITLE, + title: i18n.MACROS_DATA_INPUT_FILE_UPLOAD_TITLE, status: uploadStepStatus, children: ( ( ({ createResources, apiError, isLoading }) => { const onFileParsed = useCallback( (content: Array>) => { - const rules = content.map(formatMacroRow); - createResources(rules); + const macros = content.map(formatMacroRow); + createResources(macros); }, [createResources] ); @@ -56,14 +56,14 @@ export const MacrosFileUpload = React.memo( initialPromptText={ <> - {i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT} + {i18n.MACROS_DATA_INPUT_FILE_UPLOAD_PROMPT} } accept="application/json, application/x-ndjson" onChange={parseFile} display="large" - aria-label="Upload logs sample file" + aria-label="Upload macros file" isLoading={isParsing || isLoading} disabled={isParsing || isLoading} data-test-subj="macrosFilePicker" diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts index 25b64787d6dcd..6625b271d0f4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts @@ -7,20 +7,11 @@ import { i18n } from '@kbn/i18n'; -export const RULES_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate( +export const MACROS_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.title', { defaultMessage: 'Update your macros export' } ); -export const RULES_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate( +export const MACROS_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.prompt', { defaultMessage: 'Select or drag and drop the exported JSON file' } ); - -export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.createSuccess', - { defaultMessage: 'Macros uploaded successfully' } -); -export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.createError', - { defaultMessage: 'Failed to upload macros file' } -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx index acc22a030b02f..0c919a2db7a5d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx @@ -6,25 +6,17 @@ */ import type { EuiStepProps } from '@elastic/eui'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiStepNumber, - EuiSteps, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStepNumber, EuiTitle } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; -import { SubStepsWrapper } from '../common/sub_step_wrapper'; -import type { OnMigrationCreated, OnMissingResourcesFetched, DataInputStep } from '../../types'; +import type { OnMigrationCreated, OnMissingResourcesFetched } from '../../types'; +import * as i18n from './translations'; +import { DataInputStep } from '../constants'; +import { getStatus } from '../common/get_status'; +import { SubSteps } from '../common/sub_step'; import { useCopyExportQueryStep } from './sub_steps/copy_export_query'; import { useRulesFileUploadStep } from './sub_steps/rules_file_upload'; -import * as i18n from './translations'; import { useCheckResourcesStep } from './sub_steps/check_resources'; -import { getStatus } from '../common/get_status'; - -const DataInputStepNumber: DataInputStep = 1; interface RulesDataInputSubStepsProps { migrationStats?: RuleMigrationTaskStats; @@ -37,7 +29,7 @@ interface RulesDataInputProps extends RulesDataInputSubStepsProps { export const RulesDataInput = React.memo( ({ dataInputStep, migrationStats, onMigrationCreated, onMissingResourcesFetched }) => { const dataInputStatus = useMemo( - () => getStatus(DataInputStepNumber, dataInputStep), + () => getStatus(DataInputStep.Rules, dataInputStep), [dataInputStep] ); @@ -49,7 +41,7 @@ export const RulesDataInput = React.memo( @@ -121,11 +113,7 @@ export const RulesDataInputSubSteps = React.memo( [copyStep, uploadStep, resourcesStep] ); - return ( - - - - ); + return ; } ); RulesDataInputSubSteps.displayName = 'RulesDataInputActive'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts index b293a9394ba54..1e5a8a0f7028c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts @@ -13,10 +13,3 @@ import type { export type OnMigrationCreated = (migrationStats: RuleMigrationTaskStats) => void; export type OnResourcesCreated = () => void; export type OnMissingResourcesFetched = (missingResources: RuleMigrationResourceData[]) => void; - -export enum DataInputStep { - Rules = 1, - Macros = 2, - Lookups = 3, - End = 10, -} diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx new file mode 100644 index 0000000000000..0be6fa7b75f5a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPanel, + EuiProgress, + EuiLoadingSpinner, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import { AssistantIcon } from '@kbn/ai-assistant-icon'; +import { PanelText } from '../../../../common/components/panel_text'; +import type { RuleMigrationStats } from '../../types'; +import * as i18n from './translations'; +import { RuleMigrationsReadMore } from './read_more'; + +export interface MigrationProgressPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationProgressPanel = React.memo( + ({ migrationStats }) => { + const finishedCount = migrationStats.rules.completed + migrationStats.rules.failed; + const progressValue = (finishedCount / migrationStats.rules.total) * 100; + + const preparing = migrationStats.rules.pending === migrationStats.rules.total; + + return ( + + + + +

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

+
+
+ + + {i18n.RULE_MIGRATION_PROGRESS_DESCRIPTION(migrationStats.rules.total)} + + + + + + + + + + + {preparing ? i18n.RULE_MIGRATION_PREPARING : i18n.RULE_MIGRATION_TRANSLATING} + + + + + + + {!preparing && ( + <> + + + + + )} + +
+
+ ); + } +); +MigrationProgressPanel.displayName = 'MigrationProgressPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx new file mode 100644 index 0000000000000..3c230cba4c34f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx @@ -0,0 +1,88 @@ +/* + * 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 } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiPanel } from '@elastic/eui'; +import { CenteredLoadingSpinner } from '../../../../common/components/centered_loading_spinner'; +import type { RuleMigrationResourceData } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { PanelText } from '../../../../common/components/panel_text'; +import { useStartMigration } from '../../service/hooks/use_start_migration'; +import type { RuleMigrationStats } from '../../types'; +import { useRuleMigrationDataInputContext } from '../data_input_flyout/context'; +import * as i18n from './translations'; +import { useGetMissingResources } from '../../service/hooks/use_get_missing_resources'; + +export interface MigrationReadyPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationReadyPanel = React.memo(({ migrationStats }) => { + const { openFlyout } = useRuleMigrationDataInputContext(); + const [missingResources, setMissingResources] = React.useState([]); + const { getMissingResources, isLoading } = useGetMissingResources(setMissingResources); + + useEffect(() => { + getMissingResources(migrationStats.id); + }, [getMissingResources, migrationStats.id]); + + const onOpenFlyout = useCallback(() => { + openFlyout(migrationStats); + }, [openFlyout, migrationStats]); + + return ( + + + + + + +

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

+
+
+ + + {i18n.RULE_MIGRATION_READY_DESCRIPTION( + migrationStats.rules.total, + !isLoading && missingResources.length > 0 + ? i18n.RULE_MIGRATION_READY_MISSING_RESOURCES + : '' + )} + + +
+
+ {isLoading ? ( + + ) : ( + + {missingResources.length > 0 ? ( + + {i18n.RULE_MIGRATION_UPLOAD_BUTTON} + + ) : ( + + )} + + )} +
+
+ ); +}); +MigrationReadyPanel.displayName = 'MigrationReadyPanel'; + +const StartTranslationButton = React.memo<{ migrationId: string }>(({ migrationId }) => { + const { startMigration, isLoading } = useStartMigration(); + const onStartMigration = useCallback(() => { + startMigration(migrationId); + }, [migrationId, startMigration]); + + return ( + + {i18n.RULE_MIGRATION_START_TRANSLATION_BUTTON} + + ); +}); +StartTranslationButton.displayName = 'StartTranslationButton'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx new file mode 100644 index 0000000000000..cce11abcd8eb7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx @@ -0,0 +1,227 @@ +/* + * 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 moment from 'moment'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiHorizontalRule, + EuiIcon, + EuiBasicTable, + EuiHealth, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { Chart, BarSeries, Settings, ScaleType, DARK_THEME, LIGHT_THEME } from '@elastic/charts'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import { AssistantIcon } from '@kbn/ai-assistant-icon'; +import { PanelText } from '../../../../common/components/panel_text'; +import { + convertTranslationResultIntoText, + useResultVisColors, +} from '../../utils/translation_results'; +import type { RuleMigrationTranslationStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { useGetMigrationTranslationStats } from '../../logic/use_get_migration_translation_stats'; +import { CenteredLoadingSpinner } from '../../../../common/components/centered_loading_spinner'; +import { SecuritySolutionLinkButton } from '../../../../common/components/links'; +import type { RuleMigrationStats } from '../../types'; +import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; +import * as i18n from './translations'; + +export interface MigrationResultPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationResultPanel = React.memo(({ migrationStats }) => { + const { data: translationStats, isLoading: isLoadingTranslationStats } = + useGetMigrationTranslationStats(migrationStats.id); + return ( + + + + + +

{i18n.RULE_MIGRATION_COMPLETE_TITLE(migrationStats.number)}

+
+
+ + +

+ {i18n.RULE_MIGRATION_COMPLETE_DESCRIPTION( + moment(migrationStats.created_at).format('MMMM Do YYYY, h:mm:ss a'), + moment(migrationStats.last_updated_at).fromNow() + )} +

+
+
+
+
+ + + + + + + + + + +

{i18n.RULE_MIGRATION_SUMMARY_TITLE}

+
+
+
+
+ + + + + {isLoadingTranslationStats ? ( + + ) : ( + translationStats && ( + <> + + {i18n.RULE_MIGRATION_SUMMARY_CHART_TITLE} + + + + + ) + )} + + + + + + {i18n.RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON} + + + + + + + +
+ {/* TODO: uncomment when retry API is ready */} +
+
+ ); +}); +MigrationResultPanel.displayName = 'MigrationResultPanel'; + +const TranslationResultsChart = React.memo<{ + translationStats: RuleMigrationTranslationStats; +}>(({ translationStats }) => { + const { colorMode } = useEuiTheme(); + const translationResultColors = useResultVisColors(); + const data = [ + { + category: 'Results', + type: convertTranslationResultIntoText(RuleTranslationResult.FULL), + value: translationStats.rules.success.result.full, + }, + { + category: 'Results', + type: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL), + value: translationStats.rules.success.result.partial, + }, + { + category: 'Results', + type: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE), + value: translationStats.rules.success.result.untranslatable, + }, + { + category: 'Results', + type: i18n.RULE_MIGRATION_TRANSLATION_FAILED, + value: translationStats.rules.failed, + }, + ]; + + const colors = [ + translationResultColors[RuleTranslationResult.FULL], + translationResultColors[RuleTranslationResult.PARTIAL], + translationResultColors[RuleTranslationResult.UNTRANSLATABLE], + translationResultColors.error, + ]; + + return ( + + + + + ); +}); +TranslationResultsChart.displayName = 'TranslationResultsChart'; + +const TranslationResultsTable = React.memo<{ + translationStats: RuleMigrationTranslationStats; +}>(({ translationStats }) => { + const translationResultColors = useResultVisColors(); + const items = useMemo(() => { + return [ + { + title: convertTranslationResultIntoText(RuleTranslationResult.FULL), + value: translationStats.rules.success.result.full, + color: translationResultColors[RuleTranslationResult.FULL], + }, + { + title: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL), + value: translationStats.rules.success.result.partial, + color: translationResultColors[RuleTranslationResult.PARTIAL], + }, + { + title: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE), + value: translationStats.rules.success.result.untranslatable, + color: translationResultColors[RuleTranslationResult.UNTRANSLATABLE], + }, + { + title: i18n.RULE_MIGRATION_TRANSLATION_FAILED, + value: translationStats.rules.failed, + color: translationResultColors.error, + }, + ]; + }, [translationStats, translationResultColors]); + + return ( + {value}, + }, + { + field: 'value', + name: i18n.RULE_MIGRATION_TABLE_COLUMN_RULES, + align: 'right', + }, + ]} + /> + ); +}); +TranslationResultsTable.displayName = 'TranslationResultsTable'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx new file mode 100644 index 0000000000000..4567026f3cc08 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { PanelText } from '../../../../common/components/panel_text'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; + +export const RuleMigrationsReadMore = React.memo(() => { + const docLink = useKibana().services.docLinks.links.siem.gettingStarted; + return ( + +

+ + + + ), + }} + /> +

+
+ ); +}); +RuleMigrationsReadMore.displayName = 'RuleMigrationsReadMore'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts new file mode 100644 index 0000000000000..55e73bca32b5d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const RULE_MIGRATION_READY_DESCRIPTION = ( + totalRules: number, + missingResourcesText: string +) => + i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.ready.description', { + defaultMessage: + 'Migration of {totalRules} rules is created but the translation has not started yet. {missingResourcesText}', + values: { totalRules, missingResourcesText }, + }); +export const RULE_MIGRATION_READY_MISSING_RESOURCES = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.ready.missingResources', + { defaultMessage: 'Upload macros & lookups and start the translation process' } +); + +export const RULE_MIGRATION_START_TRANSLATION_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.translate.button', + { defaultMessage: 'Start translation' } +); +export const RULE_MIGRATION_TITLE = (number: number) => + i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.migrationTitle', { + defaultMessage: 'SIEM rules migration #{number}', + values: { number }, + }); + +export const RULE_MIGRATION_PROGRESS_DESCRIPTION = (totalRules: number) => + i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.progress.description', { + defaultMessage: `Processing migration of {totalRules} rules.`, + values: { totalRules }, + }); +export const RULE_MIGRATION_PREPARING = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.preparing', + { defaultMessage: `Preparing environment for the AI powered translation.` } +); +export const RULE_MIGRATION_TRANSLATING = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.translating', + { defaultMessage: `Translating rules` } +); + +export const RULE_MIGRATION_COMPLETE_TITLE = (number: number) => + i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.result.title', { + defaultMessage: 'SIEM rules migration #{number} complete', + values: { number }, + }); + +export const RULE_MIGRATION_COMPLETE_DESCRIPTION = (createdAt: string, finishedAt: string) => + i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.result.description', { + defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.', + values: { createdAt, finishedAt }, + }); + +export const RULE_MIGRATION_SUMMARY_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.title', + { defaultMessage: 'Translation Summary' } +); + +export const RULE_MIGRATION_SUMMARY_CHART_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.chartTitle', + { defaultMessage: 'Rules by translation status' } +); + +export const RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.button', + { defaultMessage: 'View translated rules' } +); + +export const RULE_MIGRATION_TRANSLATION_FAILED = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.failed', + { defaultMessage: 'Failed' } +); + +export const RULE_MIGRATION_TABLE_COLUMN_RESULT = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.result', + { defaultMessage: 'Result' } +); +export const RULE_MIGRATION_TABLE_COLUMN_RULES = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.rules', + { defaultMessage: 'Rules' } +); + +export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResources', + { defaultMessage: 'Upload missing Macros and Lookups.' } +); +export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResourcesDescription', + { defaultMessage: 'Click upload for step-by-step guidance to finish partially translated rules.' } +); + +export const RULE_MIGRATION_UPLOAD_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.uploadMacros.button', + { defaultMessage: 'Upload' } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx new file mode 100644 index 0000000000000..f1c6bdd71613a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx @@ -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 React, { useCallback, useEffect } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + useEuiTheme, +} from '@elastic/eui'; +import { AssistantIcon } from '@kbn/ai-assistant-icon'; +import type { SpacerSize } from '@elastic/eui/src/components/spacer/spacer'; +import type { RuleMigrationResourceData } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { PanelText } from '../../../../common/components/panel_text'; +import { useGetMissingResources } from '../../service/hooks/use_get_missing_resources'; +import * as i18n from './translations'; +import { useRuleMigrationDataInputContext } from '../data_input_flyout/context'; +import type { RuleMigrationStats } from '../../types'; + +interface RuleMigrationsUploadMissingPanelProps { + migrationStats: RuleMigrationStats; + spacerSizeTop?: SpacerSize; +} +export const RuleMigrationsUploadMissingPanel = React.memo( + ({ migrationStats, spacerSizeTop }) => { + const { euiTheme } = useEuiTheme(); + const { openFlyout } = useRuleMigrationDataInputContext(); + const [missingResources, setMissingResources] = React.useState([]); + const { getMissingResources, isLoading } = useGetMissingResources(setMissingResources); + + useEffect(() => { + getMissingResources(migrationStats.id); + }, [getMissingResources, migrationStats.id]); + + const onOpenFlyout = useCallback(() => { + openFlyout(migrationStats); + }, [migrationStats, openFlyout]); + + if (isLoading || missingResources.length === 0) { + return null; + } + return ( + <> + {spacerSizeTop && } + + + + + + + + {i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE} + + + + + {i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION} + + + + + {i18n.RULE_MIGRATION_UPLOAD_BUTTON} + + + + + + ); + } +); +RuleMigrationsUploadMissingPanel.displayName = 'RuleMigrationsUploadMissingPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/index.tsx index 4328e1b888dfd..9184c48ff75b3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/index.tsx @@ -26,7 +26,7 @@ import * as i18n from './translations'; import { convertTranslationResultIntoColor, convertTranslationResultIntoText, -} from '../../../../utils/helpers'; +} from '../../../../utils/translation_results'; import { TranslationCallOut } from './callout'; interface TranslationTabProps { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx index 07ba44d4d167e..b883934a0bdcb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx @@ -31,7 +31,7 @@ import { useGetMigrationPrebuiltRules } from '../../logic/use_get_migration_preb import * as logicI18n from '../../logic/translations'; import { BulkActions } from './bulk_actions'; import { SearchField } from './search_field'; -import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; import * as i18n from './translations'; const DEFAULT_PAGE_SIZE = 10; @@ -80,10 +80,7 @@ export const MigrationRulesTable: React.FC = React.mem const tableSelection: EuiTableSelectionType = useMemo( () => ({ selectable: (item: RuleMigration) => { - return ( - !item.elastic_rule?.id && - item.translation_result === SiemMigrationRuleTranslationResult.FULL - ); + return !item.elastic_rule?.id && item.translation_result === RuleTranslationResult.FULL; }, selectableMessage: (selectable: boolean, item: RuleMigration) => { if (selectable) { @@ -190,7 +187,7 @@ export const MigrationRulesTable: React.FC = React.mem const canMigrationRuleBeInstalled = !isLoading && !ruleMigration.elastic_rule?.id && - ruleMigration.translation_result === SiemMigrationRuleTranslationResult.FULL; + ruleMigration.translation_result === RuleTranslationResult.FULL; return ( @@ -271,7 +268,7 @@ export const MigrationRulesTable: React.FC = React.mem = { - full: euiColorVis0, - partial: euiColorVis7, - untranslatable: euiColorVis9, -}; - interface StatusBadgeProps { migrationRule: RuleMigration; 'data-test-subj'?: string; @@ -37,13 +32,14 @@ interface StatusBadgeProps { export const StatusBadge: React.FC = React.memo( ({ migrationRule, 'data-test-subj': dataTestSubj = 'translation-result' }) => { + const colors = useResultVisColors(); // Installed if (migrationRule.elastic_rule?.id) { return ( - + {i18n.RULE_STATUS_INSTALLED} @@ -67,7 +63,7 @@ export const StatusBadge: React.FC = React.memo( const translationResult = migrationRule.translation_result ?? 'untranslatable'; const displayValue = convertTranslationResultIntoText(translationResult); - const color = statusToColorMap[translationResult]; + const color = colors[translationResult]; return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts index eab3888422bae..5ed8541a4af49 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts @@ -20,7 +20,7 @@ export type UpsertResources = ( migrationId: string, data: UpsertRuleMigrationResourcesRequestBody ) => void; -export type OnSuccess = () => void; +export type OnSuccess = (data: UpsertRuleMigrationResourcesRequestBody) => void; export const useUpsertResources = (onSuccess: OnSuccess) => { const { siemMigrations, notifications } = useKibana().services; @@ -33,7 +33,7 @@ export const useUpsertResources = (onSuccess: OnSuccess) => { dispatch({ type: 'start' }); await siemMigrations.rules.upsertMigrationResources(migrationId, data); - onSuccess(); + onSuccess(data); dispatch({ type: 'success' }); } catch (err) { const apiError = err.body ?? err; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 75b7887db6525..83ead556b09cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -45,7 +45,7 @@ import * as i18n from './translations'; const NAMESPACE_TRACE_OPTIONS_SESSION_STORAGE_KEY = `${DEFAULT_ASSISTANT_NAMESPACE}.${TRACE_OPTIONS_SESSION_STORAGE_KEY}` as const; -const REQUEST_POLLING_INTERVAL_MS = 5000 as const; +const REQUEST_POLLING_INTERVAL_SECONDS = 10 as const; const CREATE_MIGRATION_BODY_BATCH_SIZE = 50 as const; export class SiemRulesMigrationsService { @@ -213,7 +213,7 @@ export class SiemRulesMigrationsService { } } - await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_MS)); + await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_SECONDS * 1000)); } while (pendingMigrationIds.length > 0); } } diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx deleted file mode 100644 index fe3fbf9945077..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - RuleMigrationTranslationResultEnum, - type RuleMigrationTranslationResult, -} from '../../../../common/siem_migrations/model/rule_migration.gen'; -import * as i18n from './translations'; - -export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => { - switch (status) { - case RuleMigrationTranslationResultEnum.full: - return 'primary'; - - case RuleMigrationTranslationResultEnum.partial: - return 'warning'; - - case RuleMigrationTranslationResultEnum.untranslatable: - return 'danger'; - - default: - throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status)); - } -}; - -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: - throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status)); - } -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts new file mode 100644 index 0000000000000..d25c252fb8fec --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts @@ -0,0 +1,47 @@ +/* + * 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 { useEuiTheme } from '@elastic/eui'; +import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; +import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; + +export const useResultVisColors = () => { + const { euiTheme } = useEuiTheme(); + return { + [RuleTranslationResult.FULL]: euiTheme.colors.vis.euiColorVis0, + [RuleTranslationResult.PARTIAL]: euiTheme.colors.vis.euiColorVis5, + [RuleTranslationResult.UNTRANSLATABLE]: euiTheme.colors.vis.euiColorVis7, + error: euiTheme.colors.vis.euiColorVis9, + }; +}; + +export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => { + switch (status) { + case RuleTranslationResult.FULL: + return 'primary'; + case RuleTranslationResult.PARTIAL: + return 'warning'; + case RuleTranslationResult.UNTRANSLATABLE: + return 'danger'; + default: + return 'subdued'; + } +}; + +export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => { + switch (status) { + case RuleTranslationResult.FULL: + return i18n.SIEM_TRANSLATION_RESULT_FULL_LABEL; + case RuleTranslationResult.PARTIAL: + return i18n.SIEM_TRANSLATION_RESULT_PARTIAL_LABEL; + case RuleTranslationResult.UNTRANSLATABLE: + return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL; + default: + return i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/translations.ts similarity index 96% rename from x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts rename to x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/translations.ts index 03f76cb833818..f0f38cfc61481 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/translations.ts @@ -24,7 +24,7 @@ export const SIEM_TRANSLATION_RESULT_PARTIAL_LABEL = i18n.translate( export const SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.translationResult.untranslatable', { - defaultMessage: 'Not translated', + defaultMessage: 'Needs manual translation', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts index 9557c5cfd652f..fde332aefbd3f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts @@ -27,6 +27,7 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = ( path: SIEM_RULE_MIGRATION_RESOURCES_PATH, access: 'internal', security: { authz: { requiredPrivileges: ['securitySolution'] } }, + options: { body: { maxBytes: 26214400 } }, // rise payload limit to 25MB }) .addVersion( { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts index 5fb7d9e0525c1..4657f2516181c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts @@ -41,6 +41,9 @@ export const registerSiemRuleMigrationsStatsRoute = ( const stats = await ruleMigrationsClient.task.getStats(migrationId); + if (stats.rules.total === 0) { + return res.noContent(); + } return res.ok({ body: stats }); } catch (err) { logger.error(err); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts index 4f9d12385e32d..ede4ccbeaa6d7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts @@ -45,6 +45,9 @@ export const registerSiemRuleMigrationsTranslationStatsRoute = ( const stats = await ruleMigrationsClient.data.rules.getTranslationStats(migrationId); + if (stats.rules.total === 0) { + return res.noContent(); + } return res.ok({ body: stats }); } catch (err) { logger.error(err); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index b483b3bdd4fbb..47bcd56e6433e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -16,7 +16,10 @@ import type { Duration, } from '@elastic/elasticsearch/lib/api/types'; import type { StoredRuleMigration } from '../types'; -import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; +import { + SiemMigrationStatus, + RuleTranslationResult, +} from '../../../../../common/siem_migrations/constants'; import { type RuleMigration, type RuleMigrationTaskStats, @@ -128,19 +131,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves an array of rule documents of a specific migrations */ async get( migrationId: string, - { filters = {}, sort = {}, from, size }: RuleMigrationGetOptions = {} + { filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetOptions = {} ): Promise<{ total: number; data: StoredRuleMigration[] }> { const index = await this.getIndexName(); const query = this.getFilterQuery(migrationId, filters); + const sort = sortParam.sortField ? getSortingOptions(sortParam) : undefined; const result = await this.esClient - .search({ - index, - query, - sort: sort.sortField ? getSortingOptions(sort) : undefined, - from, - size, - }) + .search({ index, query, sort, from, size }) .catch((error) => { this.logger.error(`Error searching rule migrations: ${error.message}`); throw error; @@ -268,8 +266,15 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient const query = this.getFilterQuery(migrationId); const aggregations = { - prebuilt: { filter: searchConditions.isPrebuilt() }, - installable: { filter: { bool: { must: searchConditions.isInstallable() } } }, + success: { + filter: { term: { status: SiemMigrationStatus.COMPLETED } }, + aggs: { + result: { terms: { field: 'translation_result' } }, + installable: { filter: { bool: { must: searchConditions.isInstallable() } } }, + prebuilt: { filter: searchConditions.isPrebuilt() }, + }, + }, + failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } }, }; const result = await this.esClient .search({ index, query, aggregations, _source: false }) @@ -278,16 +283,22 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient throw error; }); - const bucket = result.aggregations ?? {}; + const aggs = result.aggregations ?? {}; const total = this.getTotalHits(result); - const prebuilt = (bucket.prebuilt as AggregationsFilterAggregate)?.doc_count ?? 0; + const successAgg = aggs.success as AggregationsFilterAggregate; + const translationResultsAgg = successAgg.result as AggregationsStringTermsAggregate; + return { id: migrationId, rules: { total, - prebuilt, - custom: total - prebuilt, - installable: (bucket.installable as AggregationsFilterAggregate)?.doc_count ?? 0, + success: { + total: (successAgg as AggregationsFilterAggregate)?.doc_count ?? 0, + result: this.translationResultAggCount(translationResultsAgg), + installable: (successAgg.installable as AggregationsFilterAggregate)?.doc_count ?? 0, + prebuilt: (successAgg.prebuilt as AggregationsFilterAggregate)?.doc_count ?? 0, + }, + failed: (aggs.failed as AggregationsFilterAggregate)?.doc_count ?? 0, }, }; } @@ -297,10 +308,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient const index = await this.getIndexName(); const query = this.getFilterQuery(migrationId); const aggregations = { - pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } }, - processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, - completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } }, - failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } }, + status: { terms: { field: 'status' } }, createdAt: { min: { field: '@timestamp' } }, lastUpdatedAt: { max: { field: 'updated_at' } }, }; @@ -311,18 +319,16 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient throw error; }); - const bucket = result.aggregations ?? {}; + const aggs = result.aggregations ?? {}; + return { id: migrationId, rules: { total: this.getTotalHits(result), - pending: (bucket.pending as AggregationsFilterAggregate)?.doc_count ?? 0, - processing: (bucket.processing as AggregationsFilterAggregate)?.doc_count ?? 0, - completed: (bucket.completed as AggregationsFilterAggregate)?.doc_count ?? 0, - failed: (bucket.failed as AggregationsFilterAggregate)?.doc_count ?? 0, + ...this.statusAggCounts(aggs.status as AggregationsStringTermsAggregate), }, - created_at: (bucket.createdAt as AggregationsMinAggregate)?.value_as_string ?? '', - last_updated_at: (bucket.lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string ?? '', + created_at: (aggs.createdAt as AggregationsMinAggregate)?.value_as_string ?? '', + last_updated_at: (aggs.lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string ?? '', }; } @@ -331,12 +337,9 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient const index = await this.getIndexName(); const aggregations: { migrationIds: AggregationsAggregationContainer } = { migrationIds: { - terms: { field: 'migration_id', order: { createdAt: 'asc' } }, + terms: { field: 'migration_id', order: { createdAt: 'asc' }, size: 10000 }, aggregations: { - pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } }, - processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, - completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } }, - failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } }, + status: { terms: { field: 'status' } }, createdAt: { min: { field: '@timestamp' } }, lastUpdatedAt: { max: { field: 'updated_at' } }, }, @@ -355,16 +358,43 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient id: bucket.key, rules: { total: bucket.doc_count, - pending: bucket.pending?.doc_count ?? 0, - processing: bucket.processing?.doc_count ?? 0, - completed: bucket.completed?.doc_count ?? 0, - failed: bucket.failed?.doc_count ?? 0, + ...this.statusAggCounts(bucket.status), }, created_at: bucket.createdAt?.value_as_string, last_updated_at: bucket.lastUpdatedAt?.value_as_string, })); } + private statusAggCounts( + statusAgg: AggregationsStringTermsAggregate + ): Record { + const buckets = statusAgg.buckets as AggregationsStringTermsBucket[]; + return { + [SiemMigrationStatus.PENDING]: + buckets.find(({ key }) => key === SiemMigrationStatus.PENDING)?.doc_count ?? 0, + [SiemMigrationStatus.PROCESSING]: + buckets.find(({ key }) => key === SiemMigrationStatus.PROCESSING)?.doc_count ?? 0, + [SiemMigrationStatus.COMPLETED]: + buckets.find(({ key }) => key === SiemMigrationStatus.COMPLETED)?.doc_count ?? 0, + [SiemMigrationStatus.FAILED]: + buckets.find(({ key }) => key === SiemMigrationStatus.FAILED)?.doc_count ?? 0, + }; + } + + private translationResultAggCount( + resultAgg: AggregationsStringTermsAggregate + ): Record { + const buckets = resultAgg.buckets as AggregationsStringTermsBucket[]; + return { + [RuleTranslationResult.FULL]: + buckets.find(({ key }) => key === RuleTranslationResult.FULL)?.doc_count ?? 0, + [RuleTranslationResult.PARTIAL]: + buckets.find(({ key }) => key === RuleTranslationResult.PARTIAL)?.doc_count ?? 0, + [RuleTranslationResult.UNTRANSLATABLE]: + buckets.find(({ key }) => key === RuleTranslationResult.UNTRANSLATABLE)?.doc_count ?? 0, + }; + } + private getFilterQuery( migrationId: string, { status, ids, installable, prebuilt, searchTerm }: RuleMigrationFilters = {} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts index 282f783671fdc..3bd8da066a45f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts @@ -6,11 +6,11 @@ */ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; export const conditions = { isFullyTranslated(): QueryDslQueryContainer { - return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } }; + return { term: { translation_result: RuleTranslationResult.FULL } }; }, isNotInstalled(): QueryDslQueryContainer { return { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts index 942adcfcc89ec..b1165ce982293 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts @@ -6,7 +6,7 @@ */ import { END, START, StateGraph } from '@langchain/langgraph'; -import { SiemMigrationRuleTranslationResult } from '../../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../../common/siem_migrations/constants'; import { getCreateSemanticQueryNode } from './nodes/create_semantic_query'; import { getMatchPrebuiltRuleNode } from './nodes/match_prebuilt_rule'; import { getProcessQueryNode } from './nodes/process_query'; @@ -62,7 +62,7 @@ const matchedPrebuiltRuleConditional = (state: MigrateRuleState) => { if (state.elastic_rule?.prebuilt_rule_id) { return END; } - if (state.translation_result === SiemMigrationRuleTranslationResult.UNTRANSLATABLE) { + if (state.translation_result === RuleTranslationResult.UNTRANSLATABLE) { return END; } return 'processQuery'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts index e4b2162249cae..d7537fdc72dd0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts @@ -7,7 +7,7 @@ import type { Logger } from '@kbn/core/server'; import { JsonOutputParser } from '@langchain/core/output_parsers'; -import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; import type { RuleMigrationsRetriever } from '../../../retrievers'; import type { ChatModel } from '../../../util/actions_client_chat'; import type { GraphNode } from '../../types'; @@ -68,7 +68,7 @@ export const getMatchPrebuiltRuleNode = ({ id: matchedRule.installedRuleId, prebuilt_rule_id: matchedRule.rule_id, }, - translation_result: SiemMigrationRuleTranslationResult.FULL, + translation_result: RuleTranslationResult.FULL, }; } } @@ -80,7 +80,7 @@ export const getMatchPrebuiltRuleNode = ({ logger.debug( `Rule: ${state.original_rule?.title} did not match any prebuilt rule, but contains inputlookup, dropping` ); - return { translation_result: SiemMigrationRuleTranslationResult.UNTRANSLATABLE }; + return { translation_result: RuleTranslationResult.UNTRANSLATABLE }; } return {}; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts index a9047c9dc5439..66b3c0c8e7a71 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts @@ -7,7 +7,7 @@ import type { BaseMessage } from '@langchain/core/messages'; import { Annotation, messagesStateReducer } from '@langchain/langgraph'; -import type { SiemMigrationRuleTranslationResult } from '../../../../../../common/siem_migrations/constants'; +import type { RuleTranslationResult } from '../../../../../../common/siem_migrations/constants'; import type { ElasticRule, OriginalRule, @@ -31,7 +31,7 @@ export const migrateRuleState = Annotation.Root({ reducer: (current, value) => value ?? current, default: () => '', }), - translation_result: Annotation(), + translation_result: Annotation(), comments: Annotation({ reducer: (current, value) => (value ? (current ?? []).concat(value) : current), default: () => [], diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts index 463de671552c1..99d7d1439d63e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts @@ -7,7 +7,7 @@ import { END, START, StateGraph } from '@langchain/langgraph'; import { isEmpty } from 'lodash/fp'; -import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; import { getEcsMappingNode } from './nodes/ecs_mapping'; import { getFilterIndexPatternsNode } from './nodes/filter_index_patterns'; import { getFixQueryErrorsNode } from './nodes/fix_query_errors'; @@ -67,7 +67,7 @@ export function getTranslateRuleGraph({ const validationRouter = (state: TranslateRuleState) => { if ( state.validation_errors.iterations <= MAX_VALIDATION_ITERATIONS && - state.translation_result === SiemMigrationRuleTranslationResult.FULL + state.translation_result === RuleTranslationResult.FULL ) { if (!isEmpty(state.validation_errors?.esql_errors)) { return 'fixQueryErrors'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/ecs_mapping/ecs_mapping.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/ecs_mapping/ecs_mapping.ts index 6acc45a95d17b..07753432e5dbc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/ecs_mapping/ecs_mapping.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/ecs_mapping/ecs_mapping.ts @@ -7,7 +7,7 @@ import type { Logger } from '@kbn/core/server'; import type { InferenceClient } from '@kbn/inference-plugin/server'; -import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; import { getEsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base_caller'; import type { GraphNode } from '../../types'; import { SIEM_RULE_MIGRATION_CIM_ECS_MAP } from './cim_ecs_map'; @@ -58,9 +58,9 @@ export const getEcsMappingNode = ({ }; }; -const getTranslationResult = (esqlQuery: string): SiemMigrationRuleTranslationResult => { +const getTranslationResult = (esqlQuery: string): RuleTranslationResult => { if (esqlQuery.match(/\[(macro):[\s\S]*\]/)) { - return SiemMigrationRuleTranslationResult.PARTIAL; + return RuleTranslationResult.PARTIAL; } - return SiemMigrationRuleTranslationResult.FULL; + return RuleTranslationResult.FULL; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/filter_index_patterns/filter_index_patterns.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/filter_index_patterns/filter_index_patterns.ts index bb1e086bf4937..b7cbcabff2ca2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/filter_index_patterns/filter_index_patterns.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/filter_index_patterns/filter_index_patterns.ts @@ -6,7 +6,7 @@ */ import type { Logger } from '@kbn/core/server'; -import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; import type { GraphNode } from '../../types'; interface GetFilterIndexPatternsNodeParams { @@ -30,7 +30,7 @@ export const getFilterIndexPatternsNode = ({ elastic_rule: { ...state.elastic_rule, query: newQuery, - translation_result: SiemMigrationRuleTranslationResult.PARTIAL, + translation_result: RuleTranslationResult.PARTIAL, }, }; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts index d613da223038c..346df02714b67 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts @@ -7,7 +7,7 @@ import type { Logger } from '@kbn/core/server'; import type { InferenceClient } from '@kbn/inference-plugin/server'; -import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; import { getEsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base_caller'; import type { GraphNode } from '../../types'; import { ESQL_SYNTAX_TRANSLATION_PROMPT } from './prompts'; @@ -63,9 +63,9 @@ export const getTranslateRuleNode = ({ }; }; -const getTranslationResult = (esqlQuery: string): SiemMigrationRuleTranslationResult => { +const getTranslationResult = (esqlQuery: string): RuleTranslationResult => { if (esqlQuery.match(/\[(macro):[\s\S]*\]/)) { - return SiemMigrationRuleTranslationResult.PARTIAL; + return RuleTranslationResult.PARTIAL; } - return SiemMigrationRuleTranslationResult.FULL; + return RuleTranslationResult.FULL; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts index ea46238002178..873f1880d2252 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts @@ -7,7 +7,7 @@ import type { BaseMessage } from '@langchain/core/messages'; import { Annotation, messagesStateReducer } from '@langchain/langgraph'; -import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; +import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; import type { ElasticRule, OriginalRule, @@ -46,9 +46,9 @@ export const translateRuleState = Annotation.Root({ reducer: (current, value) => value ?? current, default: () => ({ iterations: 0 } as TranslateRuleValidationErrors), }), - translation_result: Annotation({ + translation_result: Annotation({ reducer: (current, value) => value ?? current, - default: () => SiemMigrationRuleTranslationResult.UNTRANSLATABLE, + default: () => RuleTranslationResult.UNTRANSLATABLE, }), comments: Annotation({ reducer: (current, value) => (value ? (current ?? []).concat(value) : current), diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index 82bde9dc795d0..7729f14bca7e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -234,6 +234,7 @@ "@kbn/react-hooks", "@kbn/index-adapter", "@kbn/core-http-server-utils", - "@kbn/llm-tasks-plugin" + "@kbn/llm-tasks-plugin", + "@kbn/ai-assistant-icon" ] }