From 60fbe6f6052ba74a0a427f0db3f39ec2739c4c71 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 22 Nov 2024 15:53:58 +0100 Subject: [PATCH 01/48] add support for topics on the onboarding page --- .../onboarding/components/onboarding.tsx | 22 ++- .../components/onboarding_body/body_config.ts | 12 +- .../cards/assistant/assistant_card.tsx | 9 +- .../assistant/assistant_check_complete.ts | 4 +- .../onboarding_body/cards/assistant/index.ts | 6 +- .../cards/assistant/translations.ts | 14 -- .../cards/attack_discovery/index.ts | 4 +- .../cards/common/card_link_button.tsx | 6 +- .../connectors/connector_cards.tsx | 12 +- .../connectors/connector_setup.tsx | 0 .../connectors}/constants.ts | 2 +- .../connectors/create_connector_popover.tsx | 2 +- .../connectors/hooks/use_load_action_types.ts | 4 +- .../connectors/missing_privileges.tsx} | 38 +++-- .../connectors/translations.ts | 0 .../onboarding_body/cards/dashboards/index.ts | 2 +- .../cards/integrations/index.ts | 2 +- .../ai_connector/ai_connector_card.tsx | 71 ++++++++++ .../ai_connector/connectors_check_complete.ts | 49 +++++++ .../siem_migrations/ai_connector/index.ts | 29 ++++ .../ai_connector/translations.ts | 23 ++++ .../siem_migrations/ai_connector/types.ts | 14 ++ .../onboarding_body/hooks/use_body_config.ts | 57 +++----- .../hooks/use_completed_cards.ts | 32 ++--- .../hooks/use_expanded_card.ts | 60 +++----- .../onboarding_body/onboarding_body.tsx | 1 - .../components/onboarding_context.tsx | 130 ++++++++++++++---- .../cards/common/link_card.test.tsx | 2 +- .../cards/common/link_card.tsx | 4 +- .../cards/demo_card/demo_card.tsx | 2 +- .../cards/teammates_card/teammates_card.tsx | 2 +- .../cards/video_card/video_card.tsx | 2 +- .../{ => onboarding_header}/constants.ts | 0 .../onboarding_header.styles.ts | 3 + .../onboarding_header/onboarding_header.tsx | 11 +- .../onboarding_header_topic_selector.tsx | 44 ++++++ .../onboarding_header/translations.ts | 20 ++- .../components/onboarding_route.tsx | 38 +++++ .../public/onboarding/config.ts | 33 +++++ .../public/onboarding/constants.ts | 19 +++ .../onboarding/hooks/use_stored_state.ts | 37 +++-- .../public/onboarding/hooks/use_topic_id.ts | 35 +++++ .../public/onboarding/hooks/use_url_detail.ts | 74 ++++++++++ .../public/onboarding/types.ts | 69 +++++++--- .../prompts/replace_resources_prompt.ts | 98 +++++++++---- 45 files changed, 824 insertions(+), 274 deletions(-) rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/{assistant => common}/connectors/connector_cards.tsx (93%) rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/{assistant => common}/connectors/connector_setup.tsx (100%) rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/{assistant => common/connectors}/constants.ts (77%) rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/{assistant => common}/connectors/create_connector_popover.tsx (96%) rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/{assistant => common}/connectors/hooks/use_load_action_types.ts (81%) rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/{assistant/connectors/missing_privileges_tooltip.tsx => common/connectors/missing_privileges.tsx} (78%) rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/{assistant => common}/connectors/translations.ts (100%) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/types.ts rename x-pack/plugins/security_solution/public/onboarding/components/{ => onboarding_header}/constants.ts (100%) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_topic_selector.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_route.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/config.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/hooks/use_topic_id.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/hooks/use_url_detail.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx index 17f4840e68dc4..caa0d9f9b79d7 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx @@ -7,17 +7,23 @@ import React from 'react'; +import { Routes, Route } from '@kbn/shared-ux-router'; import { EuiSpacer, useEuiTheme } from '@elastic/eui'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { Redirect } from 'react-router-dom'; +import { ONBOARDING_PATH } from '../../../common/constants'; import { PluginTemplateWrapper } from '../../common/components/plugin_template_wrapper'; import { CenteredLoadingSpinner } from '../../common/components/centered_loading_spinner'; import { useSpaceId } from '../../common/hooks/use_space_id'; +import { OnboardingTopicId, PAGE_CONTENT_WIDTH } from '../constants'; import { OnboardingContextProvider } from './onboarding_context'; import { OnboardingAVCBanner } from './onboarding_banner'; -import { OnboardingHeader } from './onboarding_header'; -import { OnboardingBody } from './onboarding_body'; +import { OnboardingRoute } from './onboarding_route'; import { OnboardingFooter } from './onboarding_footer'; -import { PAGE_CONTENT_WIDTH } from '../constants'; + +const topicPathParam = `:topicId(${Object.values(OnboardingTopicId) // any topics + .filter((val) => val !== OnboardingTopicId.default) // except "default" + .join('|')})?`; // optional parameter export const OnboardingPage = React.memo(() => { const spaceId = useSpaceId(); @@ -42,8 +48,14 @@ export const OnboardingPage = React.memo(() => { bottomBorder="extended" style={{ backgroundColor: euiTheme.colors.body }} > - - + + + } /> + diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts index 7f97b5c8eacd1..93690f98b48e8 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts @@ -12,8 +12,9 @@ import { dashboardsCardConfig } from './cards/dashboards'; import { rulesCardConfig } from './cards/rules'; import { alertsCardConfig } from './cards/alerts'; import { assistantCardConfig } from './cards/assistant'; +import { aiConnectorCardConfig } from './cards/siem_migrations/ai_connector'; -export const bodyConfig: OnboardingGroupConfig[] = [ +export const defaultBodyConfig: OnboardingGroupConfig[] = [ { title: i18n.translate('xpack.securitySolution.onboarding.dataGroup.title', { defaultMessage: 'Ingest your data', @@ -34,3 +35,12 @@ export const bodyConfig: OnboardingGroupConfig[] = [ cards: [assistantCardConfig], }, ]; + +export const siemMigrationsBodyConfig: OnboardingGroupConfig[] = [ + { + title: i18n.translate('xpack.securitySolution.onboarding.configure.title', { + defaultMessage: 'Configure', + }), + cards: [aiConnectorCardConfig], + }, +]; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx index b728606937020..8c6ce3034c181 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import { - EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIcon, @@ -21,10 +20,10 @@ import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; import * as i18n from './translations'; import { OnboardingCardContentPanel } from '../common/card_content_panel'; -import { ConnectorCards } from './connectors/connector_cards'; +import { ConnectorCards } from '../common/connectors/connector_cards'; import { CardCallOut } from '../common/card_callout'; import type { AssistantCardMetadata } from './types'; -import { MissingPrivilegesDescription } from './connectors/missing_privileges_tooltip'; +import { MissingPrivilegesCallOut } from '../common/connectors/missing_privileges'; export const AssistantCard: OnboardingCardComponent = ({ isCardComplete, @@ -94,9 +93,7 @@ export const AssistantCard: OnboardingCardComponent = ({ ) : ( - - - + )} ); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts index 8c0d029cee583..6242eb02bd540 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts @@ -9,7 +9,7 @@ import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugi import type { AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector'; import { i18n } from '@kbn/i18n'; import type { OnboardingCardCheckComplete } from '../../../../types'; -import { AllowedActionTypeIds } from './constants'; +import { AIActionTypeIds } from '../common/connectors/constants'; import type { AssistantCardMetadata } from './types'; export const checkAssistantCardComplete: OnboardingCardCheckComplete< @@ -21,7 +21,7 @@ export const checkAssistantCardComplete: OnboardingCardCheckComplete< } = application; const aiConnectors = allConnectors.reduce((acc: AIConnector[], connector) => { - if (!connector.isMissingSecrets && AllowedActionTypeIds.includes(connector.actionTypeId)) { + if (!connector.isMissingSecrets && AIActionTypeIds.includes(connector.actionTypeId)) { acc.push(connector); } return acc; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts index fedf975052327..4850b1ee2d865 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts @@ -25,8 +25,6 @@ export const assistantCardConfig: OnboardingCardConfig = ) ), checkComplete: checkAssistantCardComplete, - // Both capabilities are needed for this card, so we should use a double array to create an AND conditional - // (a single array would create an OR conditional between them) - capabilities: [['securitySolutionAssistant.ai-assistant']], - licenseType: 'enterprise', + capabilitiesRequired: ['securitySolutionAssistant.ai-assistant'], + licenseTypeRequired: 'enterprise', }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts index de3c111280436..1c526d4974a9a 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts @@ -35,17 +35,3 @@ export const ASSISTANT_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( defaultMessage: 'Add integrations step', } ); - -export const ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER = i18n.translate( - 'xpack.securitySolution.onboarding.assistantCard.createNewConnectorPopover', - { - defaultMessage: 'Create new connector', - } -); - -export const PRIVILEGES_MISSING_TITLE = i18n.translate( - 'xpack.securitySolution.onboarding.assistantCard.callout.title', - { - defaultMessage: 'Missing privileges', - } -); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts index 3e174caa27157..beec64bd90782 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts @@ -22,6 +22,6 @@ export const attackDiscoveryCardConfig: OnboardingCardConfig = { './attack_discovery_card' ) ), - capabilities: 'securitySolutionAttackDiscovery.attack-discovery', - licenseType: 'enterprise', + capabilitiesRequired: 'securitySolutionAttackDiscovery.attack-discovery', + licenseTypeRequired: 'enterprise', }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_link_button.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_link_button.tsx index 96466466ee4a8..35254b45968f5 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_link_button.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_link_button.tsx @@ -19,13 +19,13 @@ export const withReportCardLinkClick = ): React.FC => React.memo(function WithReportCardLinkClick({ onClick, cardId, linkId, ...rest }) { - const { reportCardLinkClicked } = useOnboardingContext(); + const { telemetry } = useOnboardingContext(); const onClickWithReport = useCallback( (ev) => { - reportCardLinkClicked(cardId, linkId); + telemetry.reportCardLinkClicked(cardId, linkId); onClick?.(ev); }, - [reportCardLinkClicked, cardId, linkId, onClick] + [telemetry, cardId, linkId, onClick] ); return ; }); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_cards.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx similarity index 93% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_cards.tsx rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx index 472459b631b0a..8a4f0b648b5c9 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_cards.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx @@ -22,16 +22,24 @@ import { useKibana } from '../../../../../../common/lib/kibana'; import { CreateConnectorPopover } from './create_connector_popover'; import { ConnectorSetup } from './connector_setup'; import * as i18n from './translations'; -import { MissingPrivilegesDescription } from './missing_privileges_tooltip'; +import { MissingPrivilegesDescription } from './missing_privileges'; interface ConnectorCardsProps { connectors?: AIConnector[]; onConnectorSaved: () => void; canCreateConnectors?: boolean; + selectedConnectorId?: string; + setSelectedConnectorId?: (id: string) => void; } export const ConnectorCards = React.memo( - ({ connectors, onConnectorSaved, canCreateConnectors }) => { + ({ + connectors, + onConnectorSaved, + canCreateConnectors, + selectedConnectorId, + setSelectedConnectorId, + }) => { const { triggersActionsUi: { actionTypeRegistry }, } = useKibana().services; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_setup.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_setup.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_setup.tsx rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_setup.tsx diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/constants.ts similarity index 77% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/constants.ts index 35811c18de471..5c9c94e369854 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/constants.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const AllowedActionTypeIds = ['.bedrock', '.gen-ai', '.gemini']; +export const AIActionTypeIds = ['.bedrock', '.gen-ai', '.gemini']; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/create_connector_popover.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/create_connector_popover.tsx rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx index 32bcd66f49249..14d25641b91db 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/create_connector_popover.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx @@ -9,7 +9,7 @@ import { css } from '@emotion/css'; import { EuiPopover, EuiLink, EuiText } from '@elastic/eui'; import { ConnectorSetup } from './connector_setup'; import * as i18n from './translations'; -import { MissingPrivilegesTooltip } from './missing_privileges_tooltip'; +import { MissingPrivilegesTooltip } from './missing_privileges'; interface CreateConnectorPopoverProps { onConnectorSaved: () => void; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/hooks/use_load_action_types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/hooks/use_load_action_types.ts similarity index 81% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/hooks/use_load_action_types.ts rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/hooks/use_load_action_types.ts index 5bdee57baafc0..48b8fdfc20d59 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/hooks/use_load_action_types.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/hooks/use_load_action_types.ts @@ -9,9 +9,9 @@ import { useMemo } from 'react'; import { useLoadActionTypes as loadActionTypes } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { IToasts } from '@kbn/core-notifications-browser'; -import { AllowedActionTypeIds } from '../../constants'; +import { AIActionTypeIds } from '../constants'; export const useFilteredActionTypes = (http: HttpSetup, toasts: IToasts) => { const { data } = loadActionTypes({ http, toasts }); - return useMemo(() => data?.filter(({ id }) => AllowedActionTypeIds.includes(id)), [data]); + return useMemo(() => data?.filter(({ id }) => AIActionTypeIds.includes(id)), [data]); }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/missing_privileges_tooltip.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/missing_privileges.tsx similarity index 78% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/missing_privileges_tooltip.tsx rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/missing_privileges.tsx index 811ef72d67634..40e211d857680 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/missing_privileges_tooltip.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/missing_privileges.tsx @@ -5,13 +5,29 @@ * 2.0. */ import React from 'react'; -import { EuiCode, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiCallOut, EuiCode, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import * as i18n from './translations'; +export const MissingPrivilegesDescription = React.memo(() => { + return ( + + {i18n.PRIVILEGES_REQUIRED_TITLE} + + +
    +
  • {i18n.REQUIRED_PRIVILEGES_CONNECTORS_ALL}
  • +
+
+
+ {i18n.CONTACT_ADMINISTRATOR} +
+ ); +}); +MissingPrivilegesDescription.displayName = 'MissingPrivilegesDescription'; + interface MissingPrivilegesTooltip { children: React.ReactElement; // EuiToolTip requires a single ReactElement child } - export const MissingPrivilegesTooltip = React.memo(({ children }) => ( (({ )); MissingPrivilegesTooltip.displayName = 'MissingPrivilegesTooltip'; -export const MissingPrivilegesDescription = React.memo(() => { +export const MissingPrivilegesCallOut = React.memo(() => { return ( - - {i18n.PRIVILEGES_REQUIRED_TITLE} - - -
    -
  • {i18n.REQUIRED_PRIVILEGES_CONNECTORS_ALL}
  • -
-
-
- {i18n.CONTACT_ADMINISTRATOR} -
+ + + ); }); -MissingPrivilegesDescription.displayName = 'MissingPrivilegesDescription'; +MissingPrivilegesCallOut.displayName = 'MissingPrivilegesCallOut'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/translations.ts rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts index 356b15f50bf9b..6d9bce2e34904 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts @@ -22,5 +22,5 @@ export const dashboardsCardConfig: OnboardingCardConfig = { './dashboards_card' ) ), - capabilities: ['dashboard.show'], + capabilitiesRequired: ['dashboard.show'], }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts index 07e80ab64f522..3568376c192cf 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts @@ -27,5 +27,5 @@ export const integrationsCardConfig: OnboardingCardConfig = ({ + checkCompleteMetadata, + checkComplete, +}) => { + const { euiTheme, colorMode } = useEuiTheme(); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + const { spaceId } = useOnboardingContext(); + + const [selectedConnectorId, setSelectedConnectorId] = useStoredSiemMigrationsConnectorId(spaceId); + + const connectors = checkCompleteMetadata?.connectors; + const canExecuteConnectors = checkCompleteMetadata?.canExecuteConnectors; + const canCreateConnectors = checkCompleteMetadata?.canCreateConnectors; + + return ( + + {canExecuteConnectors ? ( + + + + {i18n.AI_CONNECTOR_CARD_DESCRIPTION} + + + + + + + ) : ( + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default AIConnectorCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts new file mode 100644 index 0000000000000..233d46b9911a2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; +import type { AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { LocalStorageKey } from '../../../../../constants'; +import type { OnboardingCardCheckComplete } from '../../../../../types'; +import { AIActionTypeIds } from '../../common/connectors/constants'; +import type { AIConnectorCardMetadata } from './types'; + +const storage = new Storage(localStorage); + +export const checkAssistantCardComplete: OnboardingCardCheckComplete< + AIConnectorCardMetadata +> = async ({ http, application }) => { + let isComplete = false; + const allConnectors = await loadConnectors({ http }); + const { capabilities } = application; + + const aiConnectors = allConnectors.reduce((acc: AIConnector[], connector) => { + if (!connector.isMissingSecrets && AIActionTypeIds.includes(connector.actionTypeId)) { + acc.push(connector); + } + return acc; + }, []); + + const storedConnectorId = storage.get(LocalStorageKey.siemMigrationsConnectorId); + if (storedConnectorId) { + if (aiConnectors.length === 0) { + storage.remove(LocalStorageKey.siemMigrationsConnectorId); + } else { + isComplete = aiConnectors.some((connector) => connector.id === storedConnectorId); + } + } + + return { + isComplete, + metadata: { + connectors: aiConnectors, + canExecuteConnectors: Boolean(capabilities.actions?.show && capabilities.actions?.execute), + canCreateConnectors: Boolean(capabilities.actions?.save), + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts new file mode 100644 index 0000000000000..45080123889d5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AssistantAvatar } from '@kbn/elastic-assistant'; +import type { OnboardingCardConfig } from '../../../../../types'; +import { OnboardingCardId } from '../../../../../constants'; +import { AI_CONNECTOR_CARD_TITLE } from './translations'; +import { checkAssistantCardComplete } from './connectors_check_complete'; +import type { AIConnectorCardMetadata } from './types'; + +export const aiConnectorCardConfig: OnboardingCardConfig = { + id: OnboardingCardId.siemMigrationsAiConnectors, + title: AI_CONNECTOR_CARD_TITLE, + icon: AssistantAvatar, + Component: React.lazy( + () => + import( + /* webpackChunkName: "onboarding_siem_migrations_ai_connector_card" */ + './ai_connector_card' + ) + ), + checkComplete: checkAssistantCardComplete, + licenseTypeRequired: 'enterprise', +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts new file mode 100644 index 0000000000000..baca7fdfe09ae --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const AI_CONNECTOR_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.aiConnector.title', + { + defaultMessage: 'Configure AI Provider', + } +); + +export const AI_CONNECTOR_CARD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.aiConnector.description', + { + defaultMessage: + 'Choose and configure any AI provider available to use start a SIEM rules migration.', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/types.ts new file mode 100644 index 0000000000000..3e0a471da6f5c --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/types.ts @@ -0,0 +1,14 @@ +/* + * 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 { ActionConnector } from '@kbn/alerts-ui-shared'; + +export interface AIConnectorCardMetadata { + connectors: ActionConnector[]; + canExecuteConnectors: boolean; + canCreateConnectors: boolean; +} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts index f7b12e5988c0d..dda9daea8afbc 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts @@ -5,51 +5,28 @@ * 2.0. */ -import useObservable from 'react-use/lib/useObservable'; import { useMemo } from 'react'; -import { hasCapabilities } from '../../../../common/lib/capabilities'; -import { useKibana } from '../../../../common/lib/kibana/kibana_react'; -import { bodyConfig } from '../body_config'; +import { useOnboardingContext } from '../../onboarding_context'; +import { useTopicId } from '../../../hooks/use_topic_id'; import type { OnboardingGroupConfig } from '../../../types'; /** - * Hook that filters the config based on the user's capabilities and license + * Hook that returns the body config for the selected topic */ -export const useBodyConfig = () => { - const { application, licensing } = useKibana().services; - const license = useObservable(licensing.license$); - - const filteredBodyConfig = useMemo(() => { - // Return empty array when the license is not defined. It should always become defined at some point. - // This exit case prevents code dependant on the cards config (like completion checks) from running multiple times. - if (!license) { - return []; - } - return bodyConfig.reduce((filteredGroups, group) => { - const filteredCards = group.cards.filter((card) => { - if (card.capabilities) { - const cardHasCapabilities = hasCapabilities(application.capabilities, card.capabilities); - if (!cardHasCapabilities) { - return false; - } - } - - if (card.licenseType) { - const cardHasLicense = license.hasAtLeast(card.licenseType); - if (!cardHasLicense) { - return false; - } - } - - return true; - }); - - if (filteredCards.length > 0) { - filteredGroups.push({ ...group, cards: filteredCards }); +export const useBodyConfig = (): OnboardingGroupConfig[] => { + const topicId = useTopicId(); + const { config } = useOnboardingContext(); + const topicBodyConfig = useMemo(() => { + let bodyConfig: OnboardingGroupConfig[] = []; + if (topicId) { + // The selected topic should always exist in the config, but we check just in case + const topicConfig = config.get(topicId); + if (topicConfig) { + bodyConfig = topicConfig.body; } - return filteredGroups; - }, []); - }, [license, application.capabilities]); + } + return bodyConfig; + }, [config, topicId]); - return filteredBodyConfig; + return topicBodyConfig; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts index 34092bf2d5eec..fdba82e38895a 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useKibana } from '../../../../common/lib/kibana'; import { useStoredCompletedCardIds } from '../../../hooks/use_stored_state'; import type { OnboardingCardId } from '../../../constants'; import type { CheckCompleteResult, CheckCompleteResponse, - OnboardingGroupConfig, OnboardingCardConfig, + OnboardingGroupConfig, } from '../../../types'; import { useOnboardingContext } from '../../onboarding_context'; @@ -32,10 +32,9 @@ export type CardCheckCompleteResult = Partial { - const { spaceId, reportCardComplete } = useOnboardingContext(); +export const useCompletedCards = (bodyConfig: OnboardingGroupConfig[]) => { + const { spaceId, telemetry } = useOnboardingContext(); const services = useKibana().services; - const autoCheckCompletedRef = useRef(false); // Use stored state to keep localStorage in sync, and a local state to avoid unnecessary re-renders. const [storedCompleteCardIds, setStoredCompleteCardIds] = useStoredCompletedCardIds(spaceId); @@ -55,7 +54,7 @@ export const useCompletedCards = (cardsGroupConfig: OnboardingGroupConfig[]) => const isCurrentlyComplete = currentCompleteCards.includes(cardId); if (completed && !isCurrentlyComplete) { const newCompleteCardIds = [...currentCompleteCards, cardId]; - reportCardComplete(cardId, options); + telemetry.reportCardComplete(cardId, options); setStoredCompleteCardIds(newCompleteCardIds); // Keep the stored state in sync with the local state return newCompleteCardIds; } else if (!completed && isCurrentlyComplete) { @@ -66,7 +65,7 @@ export const useCompletedCards = (cardsGroupConfig: OnboardingGroupConfig[]) => return currentCompleteCards; // No change }); }, - [reportCardComplete, setStoredCompleteCardIds] // static dependencies, this function needs to be stable + [setStoredCompleteCardIds, telemetry] // static dependencies, this function needs to be stable ); const getCardCheckCompleteResult = useCallback( @@ -88,11 +87,11 @@ export const useCompletedCards = (cardsGroupConfig: OnboardingGroupConfig[]) => // Internal: stores all cards that have a checkComplete function in a flat array const cardsWithAutoCheck = useMemo( () => - cardsGroupConfig.reduce((acc, group) => { + bodyConfig.reduce((acc, group) => { acc.push(...group.cards.filter((card) => card.checkComplete)); return acc; }, []), - [cardsGroupConfig] + [bodyConfig] ); // Internal: sets the result of a checkComplete function @@ -118,9 +117,7 @@ export const useCompletedCards = (cardsGroupConfig: OnboardingGroupConfig[]) => .checkComplete?.(services) .catch((err: Error) => { services.notifications.toasts.addError(err, { title: cardConfig.title }); - return { - isComplete: false, - }; + return { isComplete: false }; }) .then((checkCompleteResult) => { processCardCheckCompleteResult(cardId, checkCompleteResult); @@ -131,19 +128,14 @@ export const useCompletedCards = (cardsGroupConfig: OnboardingGroupConfig[]) => ); useEffect(() => { - // Initial auto-check for all cards, it should run only once, after cardsGroupConfig is properly populated - if (cardsWithAutoCheck.length === 0 || autoCheckCompletedRef.current) { - return; - } - autoCheckCompletedRef.current = true; + // Initial auto-check for all body cards, it should run once per `bodyConfig` (topic) change. + // The cardsWithAutoCheck is empty when the topic is still undefined (reading from the url hash). cardsWithAutoCheck.map((card) => card .checkComplete?.(services) .catch((err: Error) => { services.notifications.toasts.addError(err, { title: card.title }); - return { - isComplete: false, - }; + return { isComplete: false }; }) .then((checkCompleteResult) => { processCardCheckCompleteResult(card.id, checkCompleteResult); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts index 131953e4b0687..743deaef46fe5 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts @@ -7,11 +7,10 @@ import { useCallback, useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; -import { useStoredExpandedCardId } from '../../../hooks/use_stored_state'; import { HEIGHT_ANIMATION_DURATION } from '../onboarding_card_panel.styles'; -import type { OnboardingCardId } from '../../../constants'; +import { type OnboardingCardId } from '../../../constants'; import type { SetExpandedCardId } from '../../../types'; -import { useOnboardingContext } from '../../onboarding_context'; +import { getCardIdFromHash, useUrlDetail } from '../../../hooks/use_url_detail'; const HEADER_OFFSET = 40; @@ -25,59 +24,36 @@ const scrollToCard = (cardId: OnboardingCardId) => { }, HEIGHT_ANIMATION_DURATION); }; -const setHash = (cardId: OnboardingCardId | null) => { - history.replaceState(null, '', cardId == null ? ' ' : `#${cardId}`); -}; - /** * This hook manages the expanded card id state in the LocalStorage and the hash in the URL. */ export const useExpandedCard = () => { - const { spaceId, reportCardOpen } = useOnboardingContext(); - const [expandedCardId, setStorageExpandedCardId] = useStoredExpandedCardId(spaceId); - const location = useLocation(); + const { setCardDetail } = useUrlDetail(); - const [documentReadyState, setReadyState] = useState(document.readyState); + // The hash in the url is the Single Source of Truth for the expanded card id + const { hash } = useLocation(); + const cardIdFromHash = getCardIdFromHash(hash); - useEffect(() => { - const readyStateListener = () => setReadyState(document.readyState); - document.addEventListener('readystatechange', readyStateListener); - return () => document.removeEventListener('readystatechange', readyStateListener); - }, []); + const [cardId, setCardId] = useState(cardIdFromHash); - // This effect implements auto-scroll in the initial render, further changes in the hash should not trigger this effect + // This effect implements auto-scroll in the initial render, it only needs to be executed once per page load. useEffect(() => { - if (documentReadyState !== 'complete') return; // Wait for page to finish loading before scrolling - let cardIdFromHash = location.hash.split('?')[0].replace('#', '') as OnboardingCardId | ''; - if (!cardIdFromHash) { - if (expandedCardId == null) return; - // If the hash is empty, but it is defined the storage we use the storage value - cardIdFromHash = expandedCardId; - setHash(cardIdFromHash); + if (cardId) { + scrollToCard(cardId); } - - // If the hash is defined and different from the storage, the hash takes precedence - if (expandedCardId !== cardIdFromHash) { - setStorageExpandedCardId(cardIdFromHash); - reportCardOpen(cardIdFromHash, { auto: true }); - } - scrollToCard(cardIdFromHash); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [documentReadyState]); + }, []); const setExpandedCardId = useCallback( - (cardId, options) => { - setStorageExpandedCardId(cardId); - setHash(cardId); - if (cardId != null) { - reportCardOpen(cardId); - if (options?.scroll) { - scrollToCard(cardId); - } + (newCardId, options) => { + setCardId(newCardId); + setCardDetail(newCardId); + if (newCardId != null && options?.scroll) { + scrollToCard(newCardId); } }, - [setStorageExpandedCardId, reportCardOpen] + [setCardDetail] ); - return { expandedCardId, setExpandedCardId }; + return { expandedCardId: cardId, setExpandedCardId }; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx index 3209028e1f0cd..0b55db750c080 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx @@ -17,7 +17,6 @@ import { useCompletedCards } from './hooks/use_completed_cards'; export const OnboardingBody = React.memo(() => { const bodyConfig = useBodyConfig(); - const { expandedCardId, setExpandedCardId } = useExpandedCard(); const { isCardComplete, setCardComplete, getCardCheckCompleteResult, checkCardComplete } = useCompletedCards(bodyConfig); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx index 2a6597628a26d..337a6e6160453 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx @@ -6,46 +6,43 @@ */ import type { PropsWithChildren } from 'react'; -import React, { createContext, useContext, useMemo } from 'react'; +import React, { createContext, useCallback, useContext, useMemo } from 'react'; import { useKibana } from '../../common/lib/kibana/kibana_react'; -import type { OnboardingCardId } from '../constants'; +import type { OnboardingTopicId, OnboardingCardId } from '../constants'; import { OnboardingHubEventTypes } from '../../common/lib/telemetry'; +import { useLicense } from '../../common/hooks/use_license'; +import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; -export interface OnboardingContextValue { - spaceId: string; +import { hasCapabilities } from '../../common/lib/capabilities'; +import { onboardingConfig } from '../config'; +import type { + OnboardingConfigAvailabilityProps, + OnboardingGroupConfig, + TopicConfig, +} from '../types'; + +export interface OnboardingTelemetry { reportCardOpen: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; reportCardComplete: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; reportCardLinkClicked: (cardId: OnboardingCardId, linkId: string) => void; } + +export type OnboardingConfig = Map; +export interface OnboardingContextValue { + spaceId: string; + telemetry: OnboardingTelemetry; + config: OnboardingConfig; +} const OnboardingContext = createContext(null); export const OnboardingContextProvider: React.FC> = React.memo(({ children, spaceId }) => { - const { telemetry } = useKibana().services; + const config = useFilteredConfig(); + const telemetry = useOnboardingTelemetry(); const value = useMemo( - () => ({ - spaceId, - reportCardOpen: (cardId, { auto = false } = {}) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepOpen, { - stepId: cardId, - trigger: auto ? 'navigation' : 'click', - }); - }, - reportCardComplete: (cardId, { auto = false } = {}) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepFinished, { - stepId: cardId, - trigger: auto ? 'auto_check' : 'click', - }); - }, - reportCardLinkClicked: (cardId, linkId: string) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepLinkClicked, { - originStepId: cardId, - stepLinkId: linkId, - }); - }, - }), - [spaceId, telemetry] + () => ({ spaceId, telemetry, config }), + [spaceId, telemetry, config] ); return {children}; @@ -61,3 +58,82 @@ export const useOnboardingContext = () => { } return context; }; + +/** + * Hook that filters the config based on the user's capabilities, license and experimental features + */ +const useFilteredConfig = (): OnboardingConfig => { + const { capabilities } = useKibana().services.application; + const experimentalFeatures = ExperimentalFeaturesService.get(); + const license = useLicense(); + + const isAvailable = useCallback( + (item: OnboardingConfigAvailabilityProps) => { + if (item.experimentalFlagRequired && !experimentalFeatures[item.experimentalFlagRequired]) { + return false; + } + if (item.licenseTypeRequired && !license.isAtLeast(item.licenseTypeRequired)) { + return false; + } + if (item.capabilitiesRequired && !hasCapabilities(capabilities, item.capabilitiesRequired)) { + return false; + } + return true; + }, + [license, capabilities, experimentalFeatures] + ); + + const filteredConfig = useMemo( + () => + onboardingConfig.reduce((filteredTopicConfigs, topicConfig) => { + if (!isAvailable(topicConfig)) { + return filteredTopicConfigs; + } + const filteredBody = topicConfig.body.reduce( + (filteredGroups, group) => { + const filteredCards = group.cards.filter(isAvailable); + + if (filteredCards.length > 0) { + filteredGroups.push({ ...group, cards: filteredCards }); + } + return filteredGroups; + }, + [] + ); + if (filteredBody.length > 0) { + filteredTopicConfigs.set(topicConfig.id, { ...topicConfig, body: filteredBody }); + } + return filteredTopicConfigs; + }, new Map()), + [isAvailable] + ); + + return filteredConfig; +}; + +const useOnboardingTelemetry = (): OnboardingTelemetry => { + const { telemetry } = useKibana().services; + return useMemo( + () => ({ + reportCardOpen: (cardId, { auto = false } = {}) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepOpen, { + stepId: cardId, + trigger: auto ? 'navigation' : 'click', + }); + }, + reportCardComplete: (cardId, { auto = false } = {}) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepFinished, { + stepId: cardId, + trigger: auto ? 'auto_check' : 'click', + }); + }, + reportCardLinkClicked: (cardId, linkId: string) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepLinkClicked, { + originStepId: cardId, + stepLinkId: linkId, + }); + }, + }), + [telemetry] + ); +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx index 83bfa317d8fbb..2d6738614d146 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { LinkCard } from './link_card'; -import { OnboardingHeaderCardId, TELEMETRY_HEADER_CARD } from '../../../constants'; +import { OnboardingHeaderCardId, TELEMETRY_HEADER_CARD } from '../../constants'; import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; jest.mock('../../../../common/lib/telemetry'); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx index 12b3877628dbc..e37d054a28c1a 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx @@ -10,8 +10,8 @@ import { EuiCard, EuiImage, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elast import classNames from 'classnames'; import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; import { useCardStyles } from './link_card.styles'; -import type { OnboardingHeaderCardId } from '../../../constants'; -import { TELEMETRY_HEADER_CARD } from '../../../constants'; +import type { OnboardingHeaderCardId } from '../../constants'; +import { TELEMETRY_HEADER_CARD } from '../../constants'; interface LinkCardProps { id: OnboardingHeaderCardId; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx index b86ae2dcd219d..9daf13527108d 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx @@ -10,7 +10,7 @@ import { LinkCard } from '../common/link_card'; import demoImage from './images/demo_card.png'; import darkDemoImage from './images/demo_card_dark.png'; import * as i18n from './translations'; -import { OnboardingHeaderCardId } from '../../../constants'; +import { OnboardingHeaderCardId } from '../../constants'; export const DemoCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) => { return ( diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx index 81e6ffb3657fe..f6fbe680ab35c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx @@ -7,7 +7,7 @@ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { OnboardingHeaderCardId } from '../../../constants'; +import { OnboardingHeaderCardId } from '../../constants'; import { useOnboardingService } from '../../../../hooks/use_onboarding_service'; import { LinkCard } from '../common/link_card'; import teammatesImage from './images/teammates_card.png'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx index 2e91b7374c505..15a8950aed277 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback, useState } from 'react'; -import { OnboardingHeaderCardId } from '../../../constants'; +import { OnboardingHeaderCardId } from '../../constants'; import { OnboardingHeaderVideoModal } from './video_modal'; import * as i18n from './translations'; import videoImage from './images/video_card.png'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/constants.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/constants.ts similarity index 100% rename from x-pack/plugins/security_solution/public/onboarding/components/constants.ts rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/constants.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.styles.ts index 34cc060a97386..40cfd7a5d9e1f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.styles.ts @@ -18,5 +18,8 @@ export const useOnboardingHeaderStyles = () => { .onboardingHeaderGreetings { color: ${euiTheme.colors.darkShade}; } + .onboardingHeaderTopicSelector { + width: calc(${PAGE_CONTENT_WIDTH} / 3); + } `; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx index 0210c88186a9a..1175c125e6a81 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx @@ -17,6 +17,7 @@ import { useEuiTheme, } from '@elastic/eui'; import { useCurrentUser } from '../../../common/lib/kibana/hooks'; +import { OnboardingHeaderTopicSelector } from './onboarding_header_topic_selector'; import { useOnboardingHeaderStyles } from './onboarding_header.styles'; import rocketImage from './images/header_rocket.png'; import rocketDarkImage from './images/header_rocket_dark.png'; @@ -42,23 +43,25 @@ export const OnboardingHeader = React.memo(() => { {currentUserName && ( - {i18n.GET_STARTED_PAGE_TITLE(currentUserName)} + {i18n.ONBOARDING_PAGE_TITLE(currentUserName)} )} -

{i18n.GET_STARTED_DATA_INGESTION_HUB_SUBTITLE}

+

{i18n.ONBOARDING_PAGE_SUBTITLE}

- {i18n.GET_STARTED_DATA_INGESTION_HUB_DESCRIPTION} + {i18n.ONBOARDING_PAGE_DESCRIPTION} + +
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_topic_selector.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_topic_selector.tsx new file mode 100644 index 0000000000000..bfd36221fe153 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_topic_selector.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiButtonGroup } from '@elastic/eui'; +import type { OnboardingTopicId } from '../../constants'; +import { useOnboardingContext } from '../onboarding_context'; +import { useTopic } from '../../hooks/use_topic_id'; + +export const OnboardingHeaderTopicSelector = React.memo(() => { + const { config } = useOnboardingContext(); + const [topicId, setTopicId] = useTopic(); + + const selectorOptions = useMemo( + () => + [...config.values()].map((topicConfig) => ({ + id: topicConfig.id, + label: topicConfig.title, + })), + [config] + ); + + if (selectorOptions.length < 2) { + return null; + } + + return ( + setTopicId(id as OnboardingTopicId)} + isFullWidth + /> + ); +}); +OnboardingHeaderTopicSelector.displayName = 'OnboardingHeaderTopicSelector'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts index c1f8ca8695bb6..62eadcdcd83a6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts @@ -7,22 +7,36 @@ import { i18n } from '@kbn/i18n'; -export const GET_STARTED_PAGE_TITLE = (userName: string) => +export const ONBOARDING_PAGE_TITLE = (userName: string) => i18n.translate('xpack.securitySolution.onboarding.Title', { defaultMessage: `Hi {userName}!`, values: { userName }, }); -export const GET_STARTED_DATA_INGESTION_HUB_SUBTITLE = i18n.translate( +export const ONBOARDING_PAGE_SUBTITLE = i18n.translate( 'xpack.securitySolution.onboarding.subTitle', { defaultMessage: `Welcome to Elastic Security`, } ); -export const GET_STARTED_DATA_INGESTION_HUB_DESCRIPTION = i18n.translate( +export const ONBOARDING_PAGE_DESCRIPTION = i18n.translate( 'xpack.securitySolution.onboarding.description', { defaultMessage: `A SIEM with AI-driven security analytics, XDR and Cloud Security.`, } ); + +export const ONBOARDING_PAGE_DEFAULT_TOPIC = i18n.translate( + 'xpack.securitySolution.onboarding.topic.default', + { + defaultMessage: 'Set up security', + } +); + +export const ONBOARDING_PAGE_SIEM_MIGRATIONS_TOPIC = i18n.translate( + 'xpack.securitySolution.onboarding.topic.siemMigrations', + { + defaultMessage: 'SIEM Rule migration', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_route.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_route.tsx new file mode 100644 index 0000000000000..05224226ca9c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_route.tsx @@ -0,0 +1,38 @@ +/* + * 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, { useEffect } from 'react'; + +import type { RouteComponentProps } from 'react-router-dom'; +import { OnboardingHeader } from './onboarding_header'; +import { OnboardingBody } from './onboarding_body'; +import type { OnboardingRouteParams } from '../types'; +import { getCardIdFromHash, useUrlDetail } from '../hooks/use_url_detail'; + +type OnboardingRouteProps = RouteComponentProps; + +export const OnboardingRoute = React.memo(({ match, location }) => { + const { syncUrlDetails } = useUrlDetail(); + + /** + * This effect syncs the URL details with the stored state, it only needs to be executed once per page load. + */ + useEffect(() => { + const pathTopicId = match.params.topicId || null; + const hashCardId = getCardIdFromHash(location.hash); + syncUrlDetails(pathTopicId, hashCardId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + + + + ); +}); +OnboardingRoute.displayName = 'OnboardingContent'; diff --git a/x-pack/plugins/security_solution/public/onboarding/config.ts b/x-pack/plugins/security_solution/public/onboarding/config.ts new file mode 100644 index 0000000000000..a8f5909f9b059 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/config.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { OnboardingTopicId } from './constants'; +import { + defaultBodyConfig, + siemMigrationsBodyConfig, +} from './components/onboarding_body/body_config'; +import type { TopicConfig } from './types'; + +export const onboardingConfig: TopicConfig[] = [ + { + id: OnboardingTopicId.default, + title: i18n.translate('xpack.securitySolution.onboarding.topic.default', { + defaultMessage: 'Set up security', + }), + body: defaultBodyConfig, + }, + { + id: OnboardingTopicId.siemMigrations, + title: i18n.translate('xpack.securitySolution.onboarding.topic.siemMigrations', { + defaultMessage: 'SIEM Rule migration', + }), + body: siemMigrationsBodyConfig, + licenseTypeRequired: 'enterprise', + experimentalFlagRequired: 'siemMigrationsEnabled', + }, +]; diff --git a/x-pack/plugins/security_solution/public/onboarding/constants.ts b/x-pack/plugins/security_solution/public/onboarding/constants.ts index 0eb277bd61875..b1dc142965322 100644 --- a/x-pack/plugins/security_solution/public/onboarding/constants.ts +++ b/x-pack/plugins/security_solution/public/onboarding/constants.ts @@ -6,6 +6,11 @@ */ export const PAGE_CONTENT_WIDTH = '1150px'; +export enum OnboardingTopicId { + default = 'default', + siemMigrations = 'siem_migrations', +} + export enum OnboardingCardId { integrations = 'integrations', dashboards = 'dashboards', @@ -13,4 +18,18 @@ export enum OnboardingCardId { alerts = 'alerts', assistant = 'assistant', attackDiscovery = 'attack_discovery', + + // siem_migrations topic cards + siemMigrationsAiConnectors = 'ai_connectors', } + +export const LocalStorageKey = { + avcBannerDismissed: 'ONBOARDING_HUB.AVC_BANNER_DISMISSED', + videoVisited: 'ONBOARDING_HUB.VIDEO_VISITED', + completeCards: 'ONBOARDING_HUB.COMPLETE_CARDS', + expandedCard: 'ONBOARDING_HUB.EXPANDED_CARD', + urlDetails: 'ONBOARDING_HUB.URL_DETAILS', + selectedIntegrationTabId: 'ONBOARDING_HUB.SELECTED_INTEGRATION_TAB_ID', + integrationSearchTerm: 'ONBOARDING_HUB.INTEGRATION_SEARCH_TERM', + siemMigrationsConnectorId: 'ONBOARDING_HUB.SIEM_MIGRATIONS_CONNECTOR_ID', +} as const; diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts index eac269f3a4a35..eb73b9552e32f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts +++ b/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts @@ -6,25 +6,16 @@ */ import useLocalStorage from 'react-use/lib/useLocalStorage'; -import type { OnboardingCardId } from '../constants'; +import { useMemo } from 'react'; +import { LocalStorageKey, type OnboardingCardId } from '../constants'; import type { IntegrationTabId } from '../components/onboarding_body/cards/integrations/types'; -const LocalStorageKey = { - avcBannerDismissed: 'ONBOARDING_HUB.AVC_BANNER_DISMISSED', - videoVisited: 'ONBOARDING_HUB.VIDEO_VISITED', - completeCards: 'ONBOARDING_HUB.COMPLETE_CARDS', - expandedCard: 'ONBOARDING_HUB.EXPANDED_CARD', - selectedIntegrationTabId: 'ONBOARDING_HUB.SELECTED_INTEGRATION_TAB_ID', - IntegrationSearchTerm: 'ONBOARDING_HUB.INTEGRATION_SEARCH_TERM', - IntegrationScrollTop: 'ONBOARDING_HUB.INTEGRATION_SCROLL_TOP', -} as const; - /** * Wrapper hook for useLocalStorage, but always returns the default value when not defined instead of `undefined`. */ -const useDefinedLocalStorage = (key: string, defaultValue: T) => { +const useDefinedLocalStorage = (key: string, defaultValue: T) => { const [value, setValue] = useLocalStorage(key, defaultValue); - return [value ?? defaultValue, setValue] as const; + return useMemo(() => [value ?? defaultValue, setValue] as const, [value, defaultValue, setValue]); }; /** @@ -40,13 +31,10 @@ export const useStoredCompletedCardIds = (spaceId: string) => useDefinedLocalStorage(`${LocalStorageKey.completeCards}.${spaceId}`, []); /** - * Stores the expanded card ID per space + * Stores the selected topic ID per space */ -export const useStoredExpandedCardId = (spaceId: string) => - useDefinedLocalStorage( - `${LocalStorageKey.expandedCard}.${spaceId}`, - null - ); +export const useStoredUrlDetails = (spaceId: string) => + useDefinedLocalStorage(`${LocalStorageKey.urlDetails}.${spaceId}`, null); /** * Stores the selected integration tab ID per space @@ -65,6 +53,15 @@ export const useStoredIntegrationTabId = ( */ export const useStoredIntegrationSearchTerm = (spaceId: string) => useDefinedLocalStorage( - `${LocalStorageKey.IntegrationSearchTerm}.${spaceId}`, + `${LocalStorageKey.integrationSearchTerm}.${spaceId}`, null ); + +/** + * Stores the siem migrations connector id + */ +export const useStoredSiemMigrationsConnectorId = (spaceId: string) => + useDefinedLocalStorage( + `${LocalStorageKey.siemMigrationsConnectorId}.${spaceId}`, + undefined + ); diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_topic_id.ts b/x-pack/plugins/security_solution/public/onboarding/hooks/use_topic_id.ts new file mode 100644 index 0000000000000..b46d4ef6a8597 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/hooks/use_topic_id.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useParams } from 'react-router-dom'; +import { OnboardingTopicId } from '../constants'; +import type { OnboardingRouteParams } from '../types'; +import { useUrlDetail } from './use_url_detail'; + +/** + * Hook that returns the topic id from the URL, or the default topic id if none is present + * This is the Single Source of Truth for the topic id + */ +export const useTopicId = (): OnboardingTopicId => { + const { topicId = OnboardingTopicId.default } = useParams(); + return topicId; +}; + +export const useTopic = (): [OnboardingTopicId, (topicId: OnboardingTopicId) => void] => { + const topicId = useTopicId(); + const { setTopicDetail } = useUrlDetail(); + + const setTopicId = useCallback( + (newTopicId: OnboardingTopicId) => { + setTopicDetail(newTopicId); + }, + [setTopicDetail] + ); + + return [topicId, setTopicId]; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_url_detail.ts b/x-pack/plugins/security_solution/public/onboarding/hooks/use_url_detail.ts new file mode 100644 index 0000000000000..a00137c884191 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/hooks/use_url_detail.ts @@ -0,0 +1,74 @@ +/* + * 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 { useCallback } from 'react'; +import { SecurityPageName, useNavigateTo } from '@kbn/security-solution-navigation'; +import { useStoredUrlDetails } from './use_stored_state'; +import { OnboardingTopicId, type OnboardingCardId } from '../constants'; +import { useOnboardingContext } from '../components/onboarding_context'; +import { useTopicId } from './use_topic_id'; + +export const getCardIdFromHash = (hash: string): OnboardingCardId | null => + (hash.split('?')[0].replace('#', '') as OnboardingCardId) || null; + +const setHash = (cardId: OnboardingCardId | null) => { + history.replaceState(null, '', cardId == null ? ' ' : `#${cardId}`); +}; + +const getTopicPath = (topicId: OnboardingTopicId) => + topicId !== OnboardingTopicId.default ? topicId : ''; + +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 topicId = useTopicId(); + const [storedUrlDetail, setStoredUrlDetail] = useStoredUrlDetails(spaceId); + + const { navigateTo } = useNavigateTo(); + + const setTopicDetail = useCallback( + (newTopicId: OnboardingTopicId) => { + const path = newTopicId === OnboardingTopicId.default ? undefined : newTopicId; + setStoredUrlDetail(path ?? null); + navigateTo({ deepLinkId: SecurityPageName.landing, path }); + }, + [setStoredUrlDetail, navigateTo] + ); + + const setCardDetail = useCallback( + (newCardId: OnboardingCardId | null) => { + setHash(newCardId); + setStoredUrlDetail(`${getTopicPath(topicId)}${getCardHash(newCardId)}` || null); + if (newCardId != null) { + telemetry.reportCardOpen(newCardId); + } + }, + [setStoredUrlDetail, topicId, telemetry] + ); + + const syncUrlDetails = useCallback( + (pathTopicId: OnboardingTopicId | null, hashCardId: OnboardingCardId | null) => { + const urlDetail = `${pathTopicId || ''}${hashCardId ? `#${hashCardId}` : ''}`; + if (urlDetail && urlDetail !== storedUrlDetail) { + if (hashCardId) { + telemetry.reportCardOpen(hashCardId, { auto: true }); + } + setStoredUrlDetail(urlDetail); + } + if (!urlDetail && storedUrlDetail) { + navigateTo({ deepLinkId: SecurityPageName.landing, path: storedUrlDetail }); + } + }, + [navigateTo, setStoredUrlDetail, storedUrlDetail, telemetry] + ); + + return { setTopicDetail, setCardDetail, syncUrlDetails }; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/types.ts b/x-pack/plugins/security_solution/public/onboarding/types.ts index 9dfe1e75596db..d79dd73ced799 100644 --- a/x-pack/plugins/security_solution/public/onboarding/types.ts +++ b/x-pack/plugins/security_solution/public/onboarding/types.ts @@ -9,7 +9,8 @@ import type React from 'react'; import type { IconType } from '@elastic/eui'; import type { LicenseType } from '@kbn/licensing-plugin/public'; -import type { OnboardingCardId } from './constants'; +import type { ExperimentalFeatures } from '../../common'; +import type { OnboardingTopicId, OnboardingCardId } from './constants'; import type { RequiredCapabilities } from '../common/lib/capabilities'; import type { StartServices } from '../types'; @@ -74,31 +75,17 @@ export type OnboardingCardCheckComplete = ( services: StartServices ) => Promise>; -export interface OnboardingCardConfig { - id: OnboardingCardId; - title: string; - icon: IconType; +export interface OnboardingConfigAvailabilityProps { /** - * Component that renders the card content when expanded. - * It receives a `setComplete` function to allow the card to mark itself as complete if needed. - * Please use React.lazy() to load the component. - */ - Component: React.LazyExoticComponent>; - /** - * Function for auto-checking completion for the card - * @returns Promise for the complete status - */ - checkComplete?: OnboardingCardCheckComplete; - /** - * The RBAC capability strings required to enable the card. It uses object dot notation. e.g. `'siem.crud'`. + * The RBAC capability strings required to enable the item. It uses object dot notation. e.g. `'siem.crud'`. * * The format of the capabilities property supports OR and AND mechanism: * * To specify capabilities in an OR fashion, they can be defined in a single level array like: `capabilities: [cap1, cap2]`. - * If either of "cap1 || cap2" is granted the card will be included. + * If either of "cap1 || cap2" is granted the item will be included. * * To specify capabilities with AND conditional, use a second level array: `capabilities: [['cap1', 'cap2']]`. - * This would result in the boolean expression "cap1 && cap2", both capabilities must be granted to include the card. + * This would result in the boolean expression "cap1 && cap2", both capabilities must be granted to include the item. * * They can also be combined like: `capabilities: ['cap1', ['cap2', 'cap3']]` which would result in the boolean expression "cap1 || (cap2 && cap3)". * @@ -106,12 +93,34 @@ export interface OnboardingCardConfig { * * Default is `undefined` (no capabilities required) */ - capabilities?: RequiredCapabilities; + capabilitiesRequired?: RequiredCapabilities; /** - * Minimum license required to enable the card. + * Minimum license required to enable the item. * Default is `basic` */ - licenseType?: LicenseType; + licenseTypeRequired?: LicenseType; + /** + * The experimental features required to enable the item. + */ + experimentalFlagRequired?: keyof ExperimentalFeatures; +} + +export interface OnboardingCardConfig + extends OnboardingConfigAvailabilityProps { + id: OnboardingCardId; + title: string; + icon: IconType; + /** + * Component that renders the card content when expanded. + * It receives a `setComplete` function to allow the card to mark itself as complete if needed. + * Please use React.lazy() to load the component. + */ + Component: React.LazyExoticComponent>; + /** + * Function for auto-checking completion for the card + * @returns Promise for the complete status + */ + checkComplete?: OnboardingCardCheckComplete; } export interface OnboardingGroupConfig { @@ -120,3 +129,19 @@ export interface OnboardingGroupConfig { // eslint-disable-next-line @typescript-eslint/no-explicit-any cards: Array>; } + +export interface TopicConfig extends OnboardingConfigAvailabilityProps { + id: OnboardingTopicId; + /** + * The onboarding topic title. + */ + title: string; + /** + * The onboarding body configuration. + */ + body: OnboardingGroupConfig[]; +} + +export interface OnboardingRouteParams { + topicId?: OnboardingTopicId; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/replace_resources_prompt.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/replace_resources_prompt.ts index 45b7a36dd292d..bc90630e1ce9b 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/replace_resources_prompt.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/replace_resources_prompt.ts @@ -31,41 +31,81 @@ export const getReplaceQueryResourcesPrompt = ( ): string => { const resourcesContext = getResourcesContext(resources); return `You are an agent expert in Splunk SPL (Search Processing Language). -Your task is to inline a set of macros and lookup table values in a SPL query. - -## Guidelines: +Your task is to inline a set of macros and lookup tables syntax using their values in a SPL query. +# Guidelines - You will be provided with a SPL query and also the resources reference with the values of macros and lookup tables. -- You have to replace the macros and lookup tables in the SPL query with their actual values. +- You have to replace the macros and lookup tables syntax in the SPL query and use their values inline, if provided. - The original and modified queries must be equivalent. + +# Process +- Go through the SPL query and identify all the macros and lookup tables that are used. Two scenarios may arise: + - The macro or lookup table is provided in the resources: Replace it using its actual content. + - The macro or lookup table is not provided in the resources: Do not replace it, keep it in the query as it is. + +## Macros replacements + +### Notes: - Macros names have the number of arguments in parentheses, e.g., \`macroName(2)\`. You must replace the correct macro accounting for the number of arguments. -## Process: +### Example: + Having the following macros: + \`someSource\`: sourcetype="somesource" + \`searchTitle(1)\`: search title="$value$" + \`searchTitle\`: search title=* + \`searchType\`: search type=* + And the following SPL query: + \`\`\`spl + \`someSource\` \`someFilter\` + | \`searchTitle("sometitle")\` + | \`searchType("sometype")\` + | table * + \`\`\` + The correct replacement would be: + \`\`\`spl + sourcetype="somesource" \`someFilter\` + | search title="sometitle" + | \`searchType("sometype")\` + | table * + \`\`\` + +## Lookups replacements + +### Notes: +- OUTPUTNEW and OUTPUT fields should be replaced with the values from the lookup table. +- Use the \`case\` function to evaluate conditions in the same order provided by the lookup table. +- Ensure all lookup matching fields are correctly matched to their respective case conditions. +- If there are more than one field to match, use the \`AND\` operator to combine them inside the \`case\` function. +- The transformed SPL query should function equivalently to the original query with the \`lookup\` command. + +### Example: + Having the following lookup table: + uid,username,department + 1066,Claudia Garcia,Engineering + 1690,Rutherford Sullivan,Engineering + 1815,Vanya Patel,IT + 1862,Wei Zhang,Engineering + 1916,Alex Martin,Personnel + And the following SPL query: + \`\`\`spl + ... | lookup users uid OUTPUTNEW username, department + \`\`\` + The correct replacement would be: + \`\`\`spl + ... | eval username=case(uid=1066, "Claudia Garcia", + uid=1690, "Rutherford Sullivan", + uid=1815, "Vanya Patel", + uid=1862, "Wei Zhang", + uid=1916, "Alex Martin", + true, null), + department=case(uid=1066, "Engineering", + uid=1690, "Engineering", + uid=1815, "IT", + uid=1862, "Engineering", + uid=1916, "Personnel", + true, null) + \`\`\` -- Go through the SPL query and identify all the macros and lookup tables that are used. Two scenarios may arise: - - The macro or lookup table is provided in the resources: Replace the call by their actual value in the query. - - The macro or lookup table is not provided in the resources: Keep the call in the query as it is. - -## Example: - Having the following macros: - \`someSource\`: sourcetype="somesource" - \`searchTitle(1)\`: search title="$value$" - \`searchTitle\`: search title=* - \`searchType\`: search type=* - And the following SPL query: - \`\`\`spl - \`someSource\` \`someFilter\` - | \`searchTitle("sometitle")\` - | \`searchType("sometype")\` - | table * - \`\`\` - The correct replacement would be: - \`\`\`spl - sourcetype="somesource" \`someFilter\` - | search title="sometitle" - | \`searchType("sometype")\` - | table * - \`\`\` ## Important: You must respond only with the modified query inside a \`\`\`spl code block, nothing else. From 3985477cf48303230f3ba5272b155d9d1f604197 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 22 Nov 2024 15:56:55 +0100 Subject: [PATCH 02/48] reorganize directories inside components --- .../{ => components}/hooks/use_onboarding_service.ts | 2 +- .../onboarding/{ => components}/hooks/use_stored_state.ts | 4 ++-- .../public/onboarding/{ => components}/hooks/use_topic_id.ts | 4 ++-- .../onboarding/{ => components}/hooks/use_url_detail.ts | 4 ++-- .../{common => components}/lib/__mocks__/telemetry.ts | 0 .../public/onboarding/{common => components}/lib/telemetry.ts | 0 .../components/onboarding_banner/onboarding_banner.tsx | 2 +- .../integrations/callouts/agent_required_callout.test.tsx | 2 +- .../cards/integrations/callouts/agent_required_callout.tsx | 2 +- .../callouts/agentless_available_callout.test.tsx | 2 +- .../integrations/callouts/agentless_available_callout.tsx | 2 +- .../cards/integrations/callouts/endpoint_callout.test.tsx | 2 +- .../cards/integrations/callouts/endpoint_callout.tsx | 2 +- .../callouts/integration_card_top_callout.test.tsx | 2 +- .../integrations/callouts/integration_card_top_callout.tsx | 2 +- .../integrations/callouts/manage_integrations_callout.tsx | 2 +- .../cards/integrations/integration_card_grid_tabs.test.tsx | 4 ++-- .../cards/integrations/integration_card_grid_tabs.tsx | 4 ++-- .../cards/integrations/use_integration_card_list.test.ts | 2 +- .../cards/integrations/use_integration_card_list.ts | 2 +- .../cards/siem_migrations/ai_connector/ai_connector_card.tsx | 2 +- .../components/onboarding_body/hooks/use_body_config.ts | 2 +- .../components/onboarding_body/hooks/use_completed_cards.ts | 2 +- .../components/onboarding_body/hooks/use_expanded_card.ts | 2 +- .../components/onboarding_footer/onboarding_footer.test.tsx | 2 +- .../components/onboarding_footer/onboarding_footer.tsx | 2 +- .../onboarding_header/cards/common/link_card.test.tsx | 2 +- .../components/onboarding_header/cards/common/link_card.tsx | 2 +- .../onboarding_header/cards/teammates_card/teammates_card.tsx | 2 +- .../onboarding_header/onboarding_header_topic_selector.tsx | 2 +- .../public/onboarding/components/onboarding_route.tsx | 2 +- 31 files changed, 34 insertions(+), 34 deletions(-) rename x-pack/plugins/security_solution/public/onboarding/{ => components}/hooks/use_onboarding_service.ts (82%) rename x-pack/plugins/security_solution/public/onboarding/{ => components}/hooks/use_stored_state.ts (92%) rename x-pack/plugins/security_solution/public/onboarding/{ => components}/hooks/use_topic_id.ts (90%) rename x-pack/plugins/security_solution/public/onboarding/{ => components}/hooks/use_url_detail.ts (94%) rename x-pack/plugins/security_solution/public/onboarding/{common => components}/lib/__mocks__/telemetry.ts (100%) rename x-pack/plugins/security_solution/public/onboarding/{common => components}/lib/telemetry.ts (100%) diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_onboarding_service.ts b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_onboarding_service.ts similarity index 82% rename from x-pack/plugins/security_solution/public/onboarding/hooks/use_onboarding_service.ts rename to x-pack/plugins/security_solution/public/onboarding/components/hooks/use_onboarding_service.ts index 55f3ecb8d4aca..3d94d81530289 100644 --- a/x-pack/plugins/security_solution/public/onboarding/hooks/use_onboarding_service.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_onboarding_service.ts @@ -5,6 +5,6 @@ * 2.0. */ -import { useKibana } from '../../common/lib/kibana/kibana_react'; +import { useKibana } from '../../../common/lib/kibana/kibana_react'; export const useOnboardingService = () => useKibana().services.onboarding; diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts similarity index 92% rename from x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts rename to x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts index eb73b9552e32f..70317d90c933d 100644 --- a/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts @@ -7,8 +7,8 @@ import useLocalStorage from 'react-use/lib/useLocalStorage'; import { useMemo } from 'react'; -import { LocalStorageKey, type OnboardingCardId } from '../constants'; -import type { IntegrationTabId } from '../components/onboarding_body/cards/integrations/types'; +import { LocalStorageKey, type OnboardingCardId } from '../../constants'; +import type { IntegrationTabId } from '../onboarding_body/cards/integrations/types'; /** * Wrapper hook for useLocalStorage, but always returns the default value when not defined instead of `undefined`. diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_topic_id.ts b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_topic_id.ts similarity index 90% rename from x-pack/plugins/security_solution/public/onboarding/hooks/use_topic_id.ts rename to x-pack/plugins/security_solution/public/onboarding/components/hooks/use_topic_id.ts index b46d4ef6a8597..b20e8ae392b62 100644 --- a/x-pack/plugins/security_solution/public/onboarding/hooks/use_topic_id.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_topic_id.ts @@ -7,8 +7,8 @@ import { useCallback } from 'react'; import { useParams } from 'react-router-dom'; -import { OnboardingTopicId } from '../constants'; -import type { OnboardingRouteParams } from '../types'; +import { OnboardingTopicId } from '../../constants'; +import type { OnboardingRouteParams } from '../../types'; import { useUrlDetail } from './use_url_detail'; /** diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_url_detail.ts b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts similarity index 94% rename from x-pack/plugins/security_solution/public/onboarding/hooks/use_url_detail.ts rename to x-pack/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts index a00137c884191..387e9d66865b3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/hooks/use_url_detail.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts @@ -8,8 +8,8 @@ import { useCallback } from 'react'; import { SecurityPageName, useNavigateTo } from '@kbn/security-solution-navigation'; import { useStoredUrlDetails } from './use_stored_state'; -import { OnboardingTopicId, type OnboardingCardId } from '../constants'; -import { useOnboardingContext } from '../components/onboarding_context'; +import { OnboardingTopicId, type OnboardingCardId } from '../../constants'; +import { useOnboardingContext } from '../onboarding_context'; import { useTopicId } from './use_topic_id'; export const getCardIdFromHash = (hash: string): OnboardingCardId | null => diff --git a/x-pack/plugins/security_solution/public/onboarding/common/lib/__mocks__/telemetry.ts b/x-pack/plugins/security_solution/public/onboarding/components/lib/__mocks__/telemetry.ts similarity index 100% rename from x-pack/plugins/security_solution/public/onboarding/common/lib/__mocks__/telemetry.ts rename to x-pack/plugins/security_solution/public/onboarding/components/lib/__mocks__/telemetry.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/common/lib/telemetry.ts b/x-pack/plugins/security_solution/public/onboarding/components/lib/telemetry.ts similarity index 100% rename from x-pack/plugins/security_solution/public/onboarding/common/lib/telemetry.ts rename to x-pack/plugins/security_solution/public/onboarding/components/lib/telemetry.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/onboarding_banner.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/onboarding_banner.tsx index 201fae862b43c..0976dbc110cc8 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/onboarding_banner.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/onboarding_banner.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { AVCResultsBanner2024, useIsStillYear2024 } from '@kbn/avc-banner'; -import { useStoredIsAVCBannerDismissed } from '../../hooks/use_stored_state'; +import { useStoredIsAVCBannerDismissed } from '../hooks/use_stored_state'; export const OnboardingBanner = React.memo(() => { const [isAVCBannerDismissed, setIsAVCBannerDismissed] = useStoredIsAVCBannerDismissed(); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx index 53e8b6c34e8f2..73c9179a13236 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { AgentRequiredCallout } from './agent_required_callout'; import { TestProviders } from '../../../../../../common/mock/test_providers'; -import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../common/lib/telemetry'); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx index b1d18b138487b..763dfe749adba 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx @@ -12,7 +12,7 @@ import { LinkAnchor } from '../../../../../../common/components/links'; import { CardCallOut } from '../../common/card_callout'; import { useNavigation } from '../../../../../../common/lib/kibana'; import { FLEET_APP_ID, ADD_AGENT_PATH, TELEMETRY_AGENT_REQUIRED } from '../constants'; -import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; const fleetAgentLinkProps = { appId: FLEET_APP_ID, path: ADD_AGENT_PATH }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx index 7cd3b60c0c6ed..a2e8a4da1e195 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { TestProviders } from '../../../../../../common/mock/test_providers'; import { AgentlessAvailableCallout } from './agentless_available_callout'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../common/lib/telemetry'); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx index eaf8cbaa3b287..81c4db22f39ab 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx @@ -12,7 +12,7 @@ import { css } from '@emotion/react'; import { useKibana } from '../../../../../../common/lib/kibana'; import { LinkAnchor } from '../../../../../../common/components/links'; -import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; import { CardCallOut } from '../../common/card_callout'; import { TELEMETRY_AGENTLESS_LEARN_MORE } from '../constants'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx index 50ac060eba241..1a8c860be8951 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { EndpointCallout } from './endpoint_callout'; import { TestProviders } from '../../../../../../common/mock/test_providers'; -import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../common/lib/telemetry'); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx index d5b0199c9f401..b761a17901a38 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx @@ -13,7 +13,7 @@ import { css } from '@emotion/react'; import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; import { LinkAnchor } from '../../../../../../common/components/links'; import { CardCallOut } from '../../common/card_callout'; -import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; import { TELEMETRY_ENDPOINT_LEARN_MORE } from '../constants'; export const EndpointCallout = React.memo(() => { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx index e0aedafe45595..8454ef54bd915 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { of } from 'rxjs'; import { IntegrationCardTopCallout } from './integration_card_top_callout'; -import { useOnboardingService } from '../../../../../hooks/use_onboarding_service'; +import { useOnboardingService } from '../../../../hooks/use_onboarding_service'; import { IntegrationTabId } from '../types'; jest.mock('../../../../../hooks/use_onboarding_service', () => ({ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx index 3a6b5ae3be92c..40f4ae95cf088 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx @@ -8,7 +8,7 @@ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { useOnboardingService } from '../../../../../hooks/use_onboarding_service'; +import { useOnboardingService } from '../../../../hooks/use_onboarding_service'; import { AgentlessAvailableCallout } from './agentless_available_callout'; import { InstalledIntegrationsCallout } from './installed_integrations_callout'; import { IntegrationTabId } from '../types'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx index 839e5870d4b7e..4085f2310d570 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx @@ -11,7 +11,7 @@ import { EuiIcon } from '@elastic/eui'; import { LinkAnchor } from '../../../../../../common/components/links'; import { CardCallOut } from '../../common/card_callout'; import { useAddIntegrationsUrl } from '../../../../../../common/hooks/use_add_integrations_url'; -import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; import { TELEMETRY_MANAGE_INTEGRATIONS } from '../constants'; export const ManageIntegrationsCallout = React.memo( diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx index c88ffb6a598b7..a206d413fd01e 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx @@ -13,9 +13,9 @@ import * as module from '@kbn/fleet-plugin/public'; import { useStoredIntegrationSearchTerm, useStoredIntegrationTabId, -} from '../../../../hooks/use_stored_state'; +} from '../../../hooks/use_stored_state'; import { DEFAULT_TAB } from './constants'; -import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../lib/telemetry'; jest.mock('../../../onboarding_context'); jest.mock('../../../../hooks/use_stored_state'); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx index e1ce7f5cdecf1..6b5e3f60a24e1 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx @@ -14,7 +14,7 @@ import { withLazyHook } from '../../../../../common/components/with_lazy_hook'; import { useStoredIntegrationSearchTerm, useStoredIntegrationTabId, -} from '../../../../hooks/use_stored_state'; +} from '../../../hooks/use_stored_state'; import { useOnboardingContext } from '../../../onboarding_context'; import { DEFAULT_TAB, @@ -29,7 +29,7 @@ import { INTEGRATION_TABS, INTEGRATION_TABS_BY_ID } from './integration_tabs_con import { useIntegrationCardList } from './use_integration_card_list'; import { IntegrationTabId } from './types'; import { IntegrationCardTopCallout } from './callouts/integration_card_top_callout'; -import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../lib/telemetry'; export interface IntegrationsCardGridTabsProps { installedIntegrationsCount: number; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts index 19ab340276b83..f7ab4b72cf6d3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts @@ -6,7 +6,7 @@ */ import { renderHook } from '@testing-library/react-hooks'; import { useIntegrationCardList } from './use_integration_card_list'; -import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../lib/telemetry'; jest.mock('../../../../common/lib/telemetry'); jest.mock('../../../../../common/lib/kibana', () => ({ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts index ccea5299551c1..660464ba73501 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts @@ -23,7 +23,7 @@ import { TELEMETRY_INTEGRATION_CARD, } from './constants'; import type { GetAppUrl, NavigateTo } from '../../../../../common/lib/kibana'; -import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../lib/telemetry'; const addPathParamToUrl = (url: string, onboardingLink: string) => { const encoded = encodeURIComponent(onboardingLink); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index 8bad89388b630..4a2413a5c076b 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx @@ -13,7 +13,7 @@ import { useEuiTheme, COLOR_MODES_STANDARD, } from '@elastic/eui'; -import { useStoredSiemMigrationsConnectorId } from '../../../../../hooks/use_stored_state'; +import { useStoredSiemMigrationsConnectorId } from '../../../../hooks/use_stored_state'; import type { OnboardingCardComponent } from '../../../../../types'; import * as i18n from './translations'; import { OnboardingCardContentPanel } from '../../common/card_content_panel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts index dda9daea8afbc..c11edf6fc8610 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import { useOnboardingContext } from '../../onboarding_context'; -import { useTopicId } from '../../../hooks/use_topic_id'; +import { useTopicId } from '../../hooks/use_topic_id'; import type { OnboardingGroupConfig } from '../../../types'; /** diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts index fdba82e38895a..dee43ea1fc1bd 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useKibana } from '../../../../common/lib/kibana'; -import { useStoredCompletedCardIds } from '../../../hooks/use_stored_state'; +import { useStoredCompletedCardIds } from '../../hooks/use_stored_state'; import type { OnboardingCardId } from '../../../constants'; import type { CheckCompleteResult, diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts index 743deaef46fe5..4b11dcda9eb0e 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts @@ -10,7 +10,7 @@ import { useLocation } from 'react-router-dom'; import { HEIGHT_ANIMATION_DURATION } from '../onboarding_card_panel.styles'; import { type OnboardingCardId } from '../../../constants'; import type { SetExpandedCardId } from '../../../types'; -import { getCardIdFromHash, useUrlDetail } from '../../../hooks/use_url_detail'; +import { getCardIdFromHash, useUrlDetail } from '../../hooks/use_url_detail'; const HEADER_OFFSET = 40; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx index ae80d0c9273c3..8ece2df2d5fc9 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { trackOnboardingLinkClick } from '../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../lib/telemetry'; import { FooterLinkItem } from './onboarding_footer'; import { OnboardingFooterLinkItemId, TELEMETRY_FOOTER_LINK } from './constants'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx index 125d2af118d3f..9db64386be067 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx @@ -9,7 +9,7 @@ import React, { useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { useFooterStyles } from './onboarding_footer.styles'; import { useFooterItems } from './footer_items'; -import { trackOnboardingLinkClick } from '../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../lib/telemetry'; import type { OnboardingFooterLinkItemId } from './constants'; import { TELEMETRY_FOOTER_LINK } from './constants'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx index 2d6738614d146..93fdd4e63c470 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { LinkCard } from './link_card'; import { OnboardingHeaderCardId, TELEMETRY_HEADER_CARD } from '../../constants'; -import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../lib/telemetry'; jest.mock('../../../../common/lib/telemetry'); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx index e37d054a28c1a..71ab7b007b35f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { EuiCard, EuiImage, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import classNames from 'classnames'; -import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; +import { trackOnboardingLinkClick } from '../../../lib/telemetry'; import { useCardStyles } from './link_card.styles'; import type { OnboardingHeaderCardId } from '../../constants'; import { TELEMETRY_HEADER_CARD } from '../../constants'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx index f6fbe680ab35c..0a425acd0a93f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx @@ -8,7 +8,7 @@ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { OnboardingHeaderCardId } from '../../constants'; -import { useOnboardingService } from '../../../../hooks/use_onboarding_service'; +import { useOnboardingService } from '../../../hooks/use_onboarding_service'; import { LinkCard } from '../common/link_card'; import teammatesImage from './images/teammates_card.png'; import darkTeammatesImage from './images/teammates_card_dark.png'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_topic_selector.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_topic_selector.tsx index bfd36221fe153..c949f51d23da1 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_topic_selector.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_topic_selector.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { EuiButtonGroup } from '@elastic/eui'; import type { OnboardingTopicId } from '../../constants'; import { useOnboardingContext } from '../onboarding_context'; -import { useTopic } from '../../hooks/use_topic_id'; +import { useTopic } from '../hooks/use_topic_id'; export const OnboardingHeaderTopicSelector = React.memo(() => { const { config } = useOnboardingContext(); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_route.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_route.tsx index 05224226ca9c7..6e7dca524ce81 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_route.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_route.tsx @@ -11,7 +11,7 @@ import type { RouteComponentProps } from 'react-router-dom'; import { OnboardingHeader } from './onboarding_header'; import { OnboardingBody } from './onboarding_body'; import type { OnboardingRouteParams } from '../types'; -import { getCardIdFromHash, useUrlDetail } from '../hooks/use_url_detail'; +import { getCardIdFromHash, useUrlDetail } from './hooks/use_url_detail'; type OnboardingRouteProps = RouteComponentProps; From 85579947873953ae3dd7bb48b7e5aed0fe8b7170 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 26 Nov 2024 14:46:56 +0100 Subject: [PATCH 03/48] resources schema id --- .../model/api/rules/rule_migration.gen.ts | 11 +---------- .../model/api/rules/rule_migration.schema.yaml | 10 +--------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 36728e0e928a0..463ec97dd200e 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -167,16 +167,7 @@ export type UpsertRuleMigrationResourcesRequestParamsInput = z.input< export type UpsertRuleMigrationResourcesRequestBody = z.infer< typeof UpsertRuleMigrationResourcesRequestBody >; -export const UpsertRuleMigrationResourcesRequestBody = z.array( - RuleMigrationResourceData.merge( - z.object({ - /** - * The rule resource migration id - */ - id: NonEmptyString, - }) - ) -); +export const UpsertRuleMigrationResourcesRequestBody = z.array(RuleMigrationResourceData); export type UpsertRuleMigrationResourcesRequestBodyInput = z.input< typeof UpsertRuleMigrationResourcesRequestBody >; diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index fdb589e7b45cd..b7f5f79fd2b82 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -251,15 +251,7 @@ paths: schema: type: array items: - allOf: - - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationResourceData' - - type: object - required: - - id - properties: - id: - description: The rule resource migration id - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationResourceData' responses: 200: description: Indicates migration resources have been created or updated correctly. From da3938c308c38b38cdd81cc8fca9d4b64c2cb7c8 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 26 Nov 2024 20:18:33 +0100 Subject: [PATCH 04/48] update and add tests --- .../{onboarding_context_mocks.ts => mocks.ts} | 14 + .../__mocks__/onboarding_context.tsx | 13 +- .../onboarding_context.test.tsx.snap | 257 ++++++++++++++++++ .../callouts/agent_required_callout.test.tsx | 2 +- .../agentless_available_callout.test.tsx | 2 +- .../callouts/endpoint_callout.test.tsx | 2 +- .../integration_card_top_callout.test.tsx | 2 +- .../integration_card_grid_tabs.test.tsx | 5 +- .../use_integration_card_list.test.ts | 2 +- .../hooks/use_body_config.test.ts | 129 +++------ .../onboarding_body/hooks/use_body_config.ts | 10 +- .../hooks/use_completed_cards.test.ts | 29 +- .../hooks/use_completed_cards.ts | 1 - .../hooks/use_expanded_card.test.ts | 71 +---- .../hooks/use_expanded_card.ts | 20 +- .../components/onboarding_context.test.tsx | 161 +++++++++++ .../components/onboarding_context.tsx | 2 +- .../onboarding_footer.test.tsx | 2 +- .../cards/common/link_card.test.tsx | 2 +- 19 files changed, 513 insertions(+), 213 deletions(-) rename x-pack/plugins/security_solution/public/onboarding/components/__mocks__/{onboarding_context_mocks.ts => mocks.ts} (61%) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/__snapshots__/onboarding_context.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.test.tsx diff --git a/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context_mocks.ts b/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/mocks.ts similarity index 61% rename from x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context_mocks.ts rename to x-pack/plugins/security_solution/public/onboarding/components/__mocks__/mocks.ts index dcd5d681b34bf..8947800d529c3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context_mocks.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/mocks.ts @@ -14,3 +14,17 @@ export const mockReportCardOpen = jest.fn(); export const mockReportCardComplete = jest.fn(); export const mockReportCardLinkClicked = jest.fn(); + +export const telemetry = { + reportCardOpen: mockReportCardOpen, + reportCardComplete: mockReportCardComplete, + reportCardLinkClicked: mockReportCardLinkClicked, +}; +export const mockTelemetry = jest.fn(() => telemetry); + +export const onboardingContext = { + spaceId: 'default', + telemetry: mockTelemetry(), + config: new Map(), +}; +export const mockOnboardingContext = jest.fn(() => onboardingContext); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context.tsx index d1c9afcef33d6..a8b7eecf273b3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context.tsx @@ -12,17 +12,8 @@ */ import type { OnboardingContextValue } from '../onboarding_context'; -import { - mockReportCardOpen, - mockReportCardComplete, - mockReportCardLinkClicked, -} from './onboarding_context_mocks'; +import { mockOnboardingContext } from './mocks'; export const useOnboardingContext = (): OnboardingContextValue => { - return { - spaceId: 'default', - reportCardOpen: mockReportCardOpen, - reportCardComplete: mockReportCardComplete, - reportCardLinkClicked: mockReportCardLinkClicked, - }; + return mockOnboardingContext(); }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/__snapshots__/onboarding_context.test.tsx.snap b/x-pack/plugins/security_solution/public/onboarding/components/__snapshots__/onboarding_context.test.tsx.snap new file mode 100644 index 0000000000000..07275346cda1e --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/__snapshots__/onboarding_context.test.tsx.snap @@ -0,0 +1,257 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OnboardingContextProvider config when all requirements are met should return all topics config correctly 1`] = ` +Map { + "default" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "defaultCard1", + }, + ], + "id": "defaultGroup1", + }, + ], + "id": "default", + }, + "topic1" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "topic1Card1", + }, + ], + "id": "topic1Group1", + }, + ], + "capabilitiesRequired": Array [ + "capability1", + ], + "experimentalFlagRequired": "flag1", + "id": "topic1", + "licenseTypeRequired": "gold", + }, + "topic2" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "experimentalFlagRequired": "flag1", + "id": "topic2Card1", + }, + Object { + "id": "topic2Card2", + "licenseTypeRequired": "gold", + }, + Object { + "capabilitiesRequired": Array [ + "capability1", + ], + "id": "topic2Card3", + }, + ], + "id": "topic2Group1", + }, + ], + "id": "topic2", + }, +} +`; + +exports[`OnboardingContextProvider config when the required capabilities are not met should filter the topics config correctly 1`] = ` +Map { + "default" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "defaultCard1", + }, + ], + "id": "defaultGroup1", + }, + ], + "id": "default", + }, + "topic2" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "experimentalFlagRequired": "flag1", + "id": "topic2Card1", + }, + Object { + "id": "topic2Card2", + "licenseTypeRequired": "gold", + }, + ], + "id": "topic2Group1", + }, + ], + "id": "topic2", + }, +} +`; + +exports[`OnboardingContextProvider config when the required experimental flag is not met and the required license is not met either and the required capabilities are not met either should return only the default topics config 1`] = ` +Map { + "default" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "defaultCard1", + }, + ], + "id": "defaultGroup1", + }, + ], + "id": "default", + }, +} +`; + +exports[`OnboardingContextProvider config when the required experimental flag is not met and the required license is not met either should filter the topics config correctly 1`] = ` +Map { + "default" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "defaultCard1", + }, + ], + "id": "defaultGroup1", + }, + ], + "id": "default", + }, + "topic2" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "capabilitiesRequired": Array [ + "capability1", + ], + "id": "topic2Card3", + }, + ], + "id": "topic2Group1", + }, + ], + "id": "topic2", + }, +} +`; + +exports[`OnboardingContextProvider config when the required experimental flag is not met should filter the topics config correctly 1`] = ` +Map { + "default" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "defaultCard1", + }, + ], + "id": "defaultGroup1", + }, + ], + "id": "default", + }, + "topic2" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "topic2Card2", + "licenseTypeRequired": "gold", + }, + Object { + "capabilitiesRequired": Array [ + "capability1", + ], + "id": "topic2Card3", + }, + ], + "id": "topic2Group1", + }, + ], + "id": "topic2", + }, +} +`; + +exports[`OnboardingContextProvider config when the required license is not met and the required capabilities are not met either should filter the topics config correctly 1`] = ` +Map { + "default" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "defaultCard1", + }, + ], + "id": "defaultGroup1", + }, + ], + "id": "default", + }, + "topic2" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "experimentalFlagRequired": "flag1", + "id": "topic2Card1", + }, + ], + "id": "topic2Group1", + }, + ], + "id": "topic2", + }, +} +`; + +exports[`OnboardingContextProvider config when the required license is not met should filter the topics config correctly 1`] = ` +Map { + "default" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "id": "defaultCard1", + }, + ], + "id": "defaultGroup1", + }, + ], + "id": "default", + }, + "topic2" => Object { + "body": Array [ + Object { + "cards": Array [ + Object { + "experimentalFlagRequired": "flag1", + "id": "topic2Card1", + }, + Object { + "capabilitiesRequired": Array [ + "capability1", + ], + "id": "topic2Card3", + }, + ], + "id": "topic2Group1", + }, + ], + "id": "topic2", + }, +} +`; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx index 73c9179a13236..4f5ae2f919d66 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx @@ -17,7 +17,7 @@ import { TestProviders } from '../../../../../../common/mock/test_providers'; import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; jest.mock('../../../../../../common/lib/kibana'); -jest.mock('../../../../../common/lib/telemetry'); +jest.mock('../../../../lib/telemetry'); describe('AgentRequiredCallout', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx index a2e8a4da1e195..e761381747f46 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx @@ -13,7 +13,7 @@ import { useKibana } from '../../../../../../common/lib/kibana'; import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; jest.mock('../../../../../../common/lib/kibana'); -jest.mock('../../../../../common/lib/telemetry'); +jest.mock('../../../../lib/telemetry'); describe('AgentlessAvailableCallout', () => { const mockUseKibana = useKibana as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx index 1a8c860be8951..7d89003359743 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx @@ -17,7 +17,7 @@ import { TestProviders } from '../../../../../../common/mock/test_providers'; import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; jest.mock('../../../../../../common/lib/kibana'); -jest.mock('../../../../../common/lib/telemetry'); +jest.mock('../../../../lib/telemetry'); describe('EndpointCallout', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx index 8454ef54bd915..9cf346aeed901 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx @@ -12,7 +12,7 @@ import { IntegrationCardTopCallout } from './integration_card_top_callout'; import { useOnboardingService } from '../../../../hooks/use_onboarding_service'; import { IntegrationTabId } from '../types'; -jest.mock('../../../../../hooks/use_onboarding_service', () => ({ +jest.mock('../../../../hooks/use_onboarding_service', () => ({ useOnboardingService: jest.fn(), })); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx index a206d413fd01e..4338fd400e6a3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx @@ -18,9 +18,8 @@ import { DEFAULT_TAB } from './constants'; import { trackOnboardingLinkClick } from '../../../lib/telemetry'; jest.mock('../../../onboarding_context'); -jest.mock('../../../../hooks/use_stored_state'); -jest.mock('../../../../common/lib/telemetry'); - +jest.mock('../../../hooks/use_stored_state'); +jest.mock('../../../lib/telemetry'); jest.mock('../../../../../common/lib/kibana', () => ({ ...jest.requireActual('../../../../../common/lib/kibana'), useNavigation: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts index f7ab4b72cf6d3..095b2f988e59c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts @@ -8,7 +8,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useIntegrationCardList } from './use_integration_card_list'; import { trackOnboardingLinkClick } from '../../../lib/telemetry'; -jest.mock('../../../../common/lib/telemetry'); +jest.mock('../../../lib/telemetry'); jest.mock('../../../../../common/lib/kibana', () => ({ ...jest.requireActual('../../../../../common/lib/kibana'), useNavigation: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts index 775ff09546fe6..c2c89594669c9 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts @@ -6,112 +6,59 @@ */ import { renderHook } from '@testing-library/react-hooks'; import { useBodyConfig } from './use_body_config'; -import { useKibana } from '../../../../common/lib/kibana/kibana_react'; -import useObservable from 'react-use/lib/useObservable'; -import { hasCapabilities } from '../../../../common/lib/capabilities'; +import { mockOnboardingContext, onboardingContext } from '../../__mocks__/mocks'; -const bodyConfig = [ - { - title: 'Group 1', - cards: [ - { - id: 'license_card', - title: 'licensed card', - icon: 'fleetApp', - licenseType: 'platinum', - }, - { - id: 'capabilities_card', - title: 'rbac card', - icon: 'fleetApp', - capabilities: ['siem.crud'], - }, - ], - }, - { - title: 'Group 2', - cards: [ - { - id: 'capabilities_license_card', - title: 'all card', - icon: 'fleetApp', - capabilities: ['siem.crud'], - licenseType: 'platinum', - }, - ], - }, -]; +const topicId = 'topic-id'; +const mockUseTopicId = jest.fn(() => topicId); +jest.mock('../../hooks/use_topic_id', () => ({ + useTopicId: () => mockUseTopicId(), +})); -// Mock dependencies -jest.mock('react-use/lib/useObservable'); -jest.mock('../../../../common/lib/kibana/kibana_react'); -jest.mock('../../../../common/lib/capabilities'); -jest.mock('../body_config', () => ({ bodyConfig })); +const defaultBodyConfig = [{ title: 'Default Group 1', cards: [] }]; +const bodyConfig = [{ title: 'Group 1', cards: [] }]; +const config = new Map([ + ['default', { body: defaultBodyConfig }], + [topicId, { body: bodyConfig }], +]); -const mockLicenseHasAtLeast = jest.fn(); -const mockUseObservable = useObservable as jest.Mock; -const mockHasCapabilities = hasCapabilities as jest.Mock; -mockUseObservable.mockReturnValue({ hasAtLeast: mockLicenseHasAtLeast }); - -(useKibana as jest.Mock).mockReturnValue({ - services: { application: { capabilities: {} }, licensing: {} }, -}); +jest.mock('../../onboarding_context'); describe('useBodyConfig', () => { beforeEach(() => { - mockLicenseHasAtLeast.mockReturnValue(true); - mockHasCapabilities.mockReturnValue(true); jest.clearAllMocks(); }); - it('should return an empty array if license is not defined', () => { - mockUseObservable.mockReturnValueOnce(undefined); - const { result } = renderHook(useBodyConfig); - expect(result.current).toEqual([]); - }); + describe('when the selected topic does not have a body config', () => { + beforeEach(() => { + mockOnboardingContext.mockReturnValue({ ...onboardingContext, config: new Map() }); + }); - it('should return all cards if no capabilities or licenseType are filtered', () => { - const { result } = renderHook(useBodyConfig); - expect(result.current).toEqual(bodyConfig); + it('should return an empty array', () => { + const { result } = renderHook(() => useBodyConfig()); + expect(result.current).toEqual([]); + }); }); - it('should filter out cards based on license', () => { - mockLicenseHasAtLeast.mockReturnValue(false); + describe('when the selected topic has a body config', () => { + beforeEach(() => { + mockOnboardingContext.mockReturnValue({ ...onboardingContext, config }); + }); - const { result } = renderHook(useBodyConfig); - - expect(result.current).toEqual([ - { - title: 'Group 1', - cards: [ - { - id: 'capabilities_card', - title: 'rbac card', - icon: 'fleetApp', - capabilities: ['siem.crud'], - }, - ], - }, - ]); + it('should return the body config for the selected topic', () => { + const { result } = renderHook(() => useBodyConfig()); + expect(result.current).toEqual(bodyConfig); + }); }); - it('should filter out cards based on capabilities', () => { - mockHasCapabilities.mockReturnValue(false); - - const { result } = renderHook(useBodyConfig); + describe('when the selected topic does not exist (not expected)', () => { + beforeEach(() => { + mockUseTopicId.mockReturnValue('non-existent-topic'); + mockOnboardingContext.mockReturnValue({ ...onboardingContext, config }); + }); - expect(result.current).toEqual([ - { - title: 'Group 1', - cards: [ - { - id: 'license_card', - title: 'licensed card', - icon: 'fleetApp', - licenseType: 'platinum', - }, - ], - }, - ]); + it('should return the body config for the selected topic', () => { + const { result } = renderHook(() => useBodyConfig()); + expect(result.current).toEqual([]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts index c11edf6fc8610..0d6a26a3439d6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts @@ -18,12 +18,10 @@ export const useBodyConfig = (): OnboardingGroupConfig[] => { const { config } = useOnboardingContext(); const topicBodyConfig = useMemo(() => { let bodyConfig: OnboardingGroupConfig[] = []; - if (topicId) { - // The selected topic should always exist in the config, but we check just in case - const topicConfig = config.get(topicId); - if (topicConfig) { - bodyConfig = topicConfig.body; - } + const topicConfig = config.get(topicId); + // The selected topic should always exist in the config, but we check just in case + if (topicConfig) { + bodyConfig = topicConfig.body; } return bodyConfig; }, [config, topicId]); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.test.ts index 2c9fcd573f0d6..1ace059a5115e 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.test.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.test.ts @@ -6,11 +6,10 @@ */ import { renderHook, act, type RenderHookResult } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; import { useCompletedCards } from './use_completed_cards'; import type { OnboardingGroupConfig } from '../../../types'; import type { OnboardingCardId } from '../../../constants'; -import { mockReportCardComplete } from '../../__mocks__/onboarding_context_mocks'; +import { mockReportCardComplete } from '../../__mocks__/mocks'; import { useKibana } from '../../../../common/lib/kibana'; const defaultStoredCompletedCardIds: OnboardingCardId[] = []; @@ -20,8 +19,8 @@ const mockUseStoredCompletedCardIds = jest.fn(() => [ defaultStoredCompletedCardIds, mockSetStoredCompletedCardIds, ]); -jest.mock('../../../hooks/use_stored_state', () => ({ - ...jest.requireActual('../../../hooks/use_stored_state'), +jest.mock('../../hooks/use_stored_state', () => ({ + ...jest.requireActual('../../hooks/use_stored_state'), useStoredCompletedCardIds: () => mockUseStoredCompletedCardIds(), })); @@ -99,6 +98,8 @@ const mockFailureCardsGroupConfig = [ }, ] as unknown as OnboardingGroupConfig[]; +const flushPromises = () => new Promise(setImmediate); + describe('useCompletedCards Hook', () => { beforeEach(() => { jest.clearAllMocks(); @@ -114,11 +115,7 @@ describe('useCompletedCards Hook', () => { services: { notifications: { toasts: { addError: mockAddError } } }, }); renderResult = renderHook(useCompletedCards, { initialProps: mockFailureCardsGroupConfig }); - await act(async () => { - await waitFor(() => { - expect(mockSetStoredCompletedCardIds).toHaveBeenCalledTimes(0); // number of completed cards - }); - }); + await act(flushPromises); }); describe('when a the auto check is called', () => { @@ -158,11 +155,7 @@ describe('useCompletedCards Hook', () => { >; beforeEach(async () => { renderResult = renderHook(useCompletedCards, { initialProps: mockCardsGroupConfig }); - await act(async () => { - await waitFor(() => { - expect(mockSetStoredCompletedCardIds).toHaveBeenCalledTimes(4); // number of completed cards - }); - }); + await act(flushPromises); }); it('should set the correct completed card ids', async () => { @@ -258,12 +251,8 @@ describe('useCompletedCards Hook', () => { beforeEach(async () => { jest.clearAllMocks(); cardIncomplete.checkComplete.mockResolvedValueOnce(true); - await act(async () => { - renderResult.result.current.checkCardComplete(cardIncomplete.id); - await waitFor(() => { - expect(mockSetStoredCompletedCardIds).toHaveBeenCalledTimes(1); - }); - }); + renderResult.result.current.checkCardComplete(cardIncomplete.id); + await act(flushPromises); }); it('should set the correct completed card ids', async () => { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts index dee43ea1fc1bd..8f3bcf0b618d6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts @@ -129,7 +129,6 @@ export const useCompletedCards = (bodyConfig: OnboardingGroupConfig[]) => { useEffect(() => { // Initial auto-check for all body cards, it should run once per `bodyConfig` (topic) change. - // The cardsWithAutoCheck is empty when the topic is still undefined (reading from the url hash). cardsWithAutoCheck.map((card) => card .checkComplete?.(services) diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.test.ts index 55f60e591c17d..26612d83b565f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.test.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.test.ts @@ -9,16 +9,14 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useExpandedCard } from './use_expanded_card'; import { HEIGHT_ANIMATION_DURATION } from '../onboarding_card_panel.styles'; import type { OnboardingCardId } from '../../../constants'; -import { mockReportCardOpen } from '../../__mocks__/onboarding_context_mocks'; import { waitFor } from '@testing-library/react'; const scrollTimeout = HEIGHT_ANIMATION_DURATION + 50; -const mockSetStorageExpandedCardId = jest.fn(); -const mockUseStoredExpandedCardId = jest.fn(() => [null, mockSetStorageExpandedCardId]); -jest.mock('../../../hooks/use_stored_state', () => ({ - ...jest.requireActual('../../../hooks/use_stored_state'), - useStoredExpandedCardId: () => mockUseStoredExpandedCardId(), +const mockSetCardDetail = jest.fn(); +jest.mock('../../hooks/use_url_detail', () => ({ + ...jest.requireActual('../../hooks/use_url_detail'), + useUrlDetail: () => ({ setCardDetail: mockSetCardDetail }), })); jest.mock('react-router-dom', () => ({ @@ -26,14 +24,10 @@ jest.mock('react-router-dom', () => ({ useLocation: () => ({ hash: '#card-1', pathname: '/test' }), })); -jest.mock('../../onboarding_context'); - describe('useExpandedCard Hook', () => { const mockCardId = 'card-1' as OnboardingCardId; const mockScrollTo = jest.fn(); global.window.scrollTo = mockScrollTo; - const mockReplaceState = jest.fn(); - global.history.replaceState = mockReplaceState; const mockGetElementById = jest.fn().mockReturnValue({ focus: jest.fn(), @@ -45,40 +39,11 @@ describe('useExpandedCard Hook', () => { jest.clearAllMocks(); }); - describe('when the page is loading', () => { - beforeEach(() => { - Object.defineProperty(document, 'readyState', { - value: 'loading', - configurable: true, - }); - }); - - it('should not scroll if the page is not fully loaded', async () => { - renderHook(useExpandedCard); - - // Ensure that scroll and focus were triggered - await waitFor( - () => { - expect(mockScrollTo).not.toHaveBeenCalled(); - }, - { timeout: scrollTimeout } - ); - }); - }); - describe('when the page is completely loaded', () => { beforeEach(() => { - Object.defineProperty(document, 'readyState', { - value: 'complete', - configurable: true, - }); renderHook(useExpandedCard); }); - it('should set the expanded card id from the hash', () => { - expect(mockSetStorageExpandedCardId).toHaveBeenCalledWith(mockCardId); - }); - it('should scroll to the expanded card id from the hash', async () => { // Ensure that scroll and focus were triggered await waitFor( @@ -89,10 +54,6 @@ describe('useExpandedCard Hook', () => { { timeout: scrollTimeout } ); }); - - it('should report the expanded card id from the hash', () => { - expect(mockReportCardOpen).toHaveBeenCalledWith(mockCardId, { auto: true }); - }); }); describe('when the card is expanded manually', () => { @@ -111,12 +72,8 @@ describe('useExpandedCard Hook', () => { }); }); - it('should set the expanded card id in storage', () => { - expect(mockSetStorageExpandedCardId).toHaveBeenCalledWith(mockCardId); - }); - - it('should set the URL hash', () => { - expect(mockReplaceState).toHaveBeenCalledWith(null, '', `#${mockCardId}`); + it('should set the expanded card id', () => { + expect(mockSetCardDetail).toHaveBeenCalledWith(mockCardId); }); it('should not scroll', async () => { @@ -129,10 +86,6 @@ describe('useExpandedCard Hook', () => { { timeout: scrollTimeout } ); }); - - it('should report the expanded card id', () => { - expect(mockReportCardOpen).toHaveBeenCalledWith(mockCardId); - }); }); describe('when scroll is enabled', () => { @@ -143,12 +96,8 @@ describe('useExpandedCard Hook', () => { }); }); - it('should set the expanded card id in storage', () => { - expect(mockSetStorageExpandedCardId).toHaveBeenCalledWith(mockCardId); - }); - - it('should set the URL hash', () => { - expect(mockReplaceState).toHaveBeenCalledWith(null, '', `#${mockCardId}`); + it('should set the expanded card id', () => { + expect(mockSetCardDetail).toHaveBeenCalledWith(mockCardId); }); it('should scroll', async () => { @@ -161,10 +110,6 @@ describe('useExpandedCard Hook', () => { { timeout: scrollTimeout } ); }); - - it('should report the expanded card id', () => { - expect(mockReportCardOpen).toHaveBeenCalledWith(mockCardId); - }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts index 4b11dcda9eb0e..514618390695c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { HEIGHT_ANIMATION_DURATION } from '../onboarding_card_panel.styles'; import { type OnboardingCardId } from '../../../constants'; @@ -29,20 +29,20 @@ const scrollToCard = (cardId: OnboardingCardId) => { */ export const useExpandedCard = () => { const { setCardDetail } = useUrlDetail(); - - // The hash in the url is the Single Source of Truth for the expanded card id const { hash } = useLocation(); - const cardIdFromHash = getCardIdFromHash(hash); + const cardIdFromHash = useMemo(() => getCardIdFromHash(hash), [hash]); - const [cardId, setCardId] = useState(cardIdFromHash); + const [cardId, setCardId] = useState(null); - // This effect implements auto-scroll in the initial render, it only needs to be executed once per page load. + // This effect implements auto-scroll in the initial render. useEffect(() => { - if (cardId) { - scrollToCard(cardId); + if (cardIdFromHash) { + setCardId(cardIdFromHash); + scrollToCard(cardIdFromHash); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // cardIdFromHash is only defined once on page load + // it does not change with subsequent url hash changes since history.replaceState is used + }, [cardIdFromHash]); const setExpandedCardId = useCallback( (newCardId, options) => { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.test.tsx new file mode 100644 index 0000000000000..cb395996903be --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.test.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 from 'react'; +import { renderHook } from '@testing-library/react'; +import { OnboardingContextProvider, useOnboardingContext } from './onboarding_context'; +import { useLicense } from '../../common/hooks/use_license'; +import { hasCapabilities } from '../../common/lib/capabilities'; +import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; + +jest.mock('../../common/lib/kibana/kibana_react', () => ({ + useKibana: jest.fn().mockReturnValue({ services: { application: { capabilities: {} } } }), +})); +jest.mock('../../common/lib/capabilities', () => ({ hasCapabilities: jest.fn() })); +const mockHasCapabilities = hasCapabilities as jest.Mock; + +jest.mock('../../common/hooks/use_license', () => ({ useLicense: jest.fn() })); +const mockUseLicense = useLicense as jest.Mock; + +jest.mock('../../common/experimental_features_service', () => ({ + ExperimentalFeaturesService: { get: jest.fn() }, +})); +const mockExperimentalFeatures = ExperimentalFeaturesService.get as jest.Mock; + +jest.mock('../config', () => ({ + onboardingConfig: [ + { + id: 'default', + body: [ + { + id: 'defaultGroup1', + cards: [{ id: 'defaultCard1' }], + }, + ], + }, + { + id: 'topic1', + experimentalFlagRequired: 'flag1', + licenseTypeRequired: 'gold', + capabilitiesRequired: ['capability1'], + body: [ + { + id: 'topic1Group1', + cards: [{ id: 'topic1Card1' }], + }, + ], + }, + { + id: 'topic2', + body: [ + { + id: 'topic2Group1', + cards: [ + { id: 'topic2Card1', experimentalFlagRequired: 'flag1' }, + { id: 'topic2Card2', licenseTypeRequired: 'gold' }, + { id: 'topic2Card3', capabilitiesRequired: ['capability1'] }, + ], + }, + ], + }, + ], +})); + +const wrapper: React.FC> = ({ children }) => ( + {children} +); + +describe('OnboardingContextProvider', () => { + describe('config', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockExperimentalFeatures.mockReturnValue({ flag1: true }); + mockUseLicense.mockReturnValue({ isAtLeast: jest.fn(() => true) }); + mockHasCapabilities.mockReturnValue(true); + }); + + describe('when all requirements are met', () => { + it('should return all topics config correctly', () => { + const { result } = renderHook(useOnboardingContext, { wrapper }); + expect(result.current.config.size).toEqual(3); + expect(result.current.config).toMatchSnapshot(); + }); + }); + + describe('when the required experimental flag is not met', () => { + beforeEach(() => { + mockExperimentalFeatures.mockReturnValue({}); + }); + + it('should filter the topics config correctly', () => { + const { result } = renderHook(useOnboardingContext, { wrapper }); + expect(result.current.config.size).toEqual(2); + expect(result.current.config).toMatchSnapshot(); + }); + + describe('and the required license is not met either', () => { + beforeEach(() => { + mockUseLicense.mockReturnValue({ isAtLeast: jest.fn(() => false) }); + }); + + it('should filter the topics config correctly', () => { + const { result } = renderHook(useOnboardingContext, { wrapper }); + expect(result.current.config.size).toEqual(2); + expect(result.current.config).toMatchSnapshot(); + }); + + describe('and the required capabilities are not met either', () => { + beforeEach(() => { + mockHasCapabilities.mockReturnValue(false); + }); + + it('should return only the default topics config', () => { + const { result } = renderHook(useOnboardingContext, { wrapper }); + expect(result.current.config.size).toEqual(1); + expect(result.current.config).toMatchSnapshot(); + }); + }); + }); + }); + + describe('when the required license is not met', () => { + beforeEach(() => { + mockUseLicense.mockReturnValue({ isAtLeast: jest.fn(() => false) }); + }); + + it('should filter the topics config correctly', () => { + const { result } = renderHook(useOnboardingContext, { wrapper }); + expect(result.current.config.size).toEqual(2); + expect(result.current.config).toMatchSnapshot(); + }); + + describe('and the required capabilities are not met either', () => { + beforeEach(() => { + mockHasCapabilities.mockReturnValue(false); + }); + + it('should filter the topics config correctly', () => { + const { result } = renderHook(useOnboardingContext, { wrapper }); + expect(result.current.config.size).toEqual(2); + expect(result.current.config).toMatchSnapshot(); + }); + }); + }); + + describe('when the required capabilities are not met', () => { + beforeEach(() => { + mockHasCapabilities.mockReturnValue(false); + }); + + it('should filter the topics config correctly', () => { + const { result } = renderHook(useOnboardingContext, { wrapper }); + expect(result.current.config.size).toEqual(2); + expect(result.current.config).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx index 337a6e6160453..17932207c6271 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx @@ -14,12 +14,12 @@ import { useLicense } from '../../common/hooks/use_license'; import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; import { hasCapabilities } from '../../common/lib/capabilities'; -import { onboardingConfig } from '../config'; import type { OnboardingConfigAvailabilityProps, OnboardingGroupConfig, TopicConfig, } from '../types'; +import { onboardingConfig } from '../config'; export interface OnboardingTelemetry { reportCardOpen: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx index 8ece2df2d5fc9..2b663add12248 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx @@ -11,7 +11,7 @@ import { trackOnboardingLinkClick } from '../lib/telemetry'; import { FooterLinkItem } from './onboarding_footer'; import { OnboardingFooterLinkItemId, TELEMETRY_FOOTER_LINK } from './constants'; -jest.mock('../../common/lib/telemetry'); +jest.mock('../lib/telemetry'); describe('OnboardingFooterComponent', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx index 93fdd4e63c470..febc8431627b8 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx @@ -11,7 +11,7 @@ import { LinkCard } from './link_card'; import { OnboardingHeaderCardId, TELEMETRY_HEADER_CARD } from '../../constants'; import { trackOnboardingLinkClick } from '../../../lib/telemetry'; -jest.mock('../../../../common/lib/telemetry'); +jest.mock('../../../lib/telemetry'); describe('DataIngestionHubHeaderCardComponent', () => { beforeEach(() => { From 132c67bc4da0dd6e56db6a1914879429c020339f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 27 Nov 2024 14:52:40 +0100 Subject: [PATCH 05/48] resources type fix --- .../rules/api/resources/upsert.ts | 4 ++-- .../rule_migrations_data_resources_client.ts | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts index be1f3e84c46ea..645fa09b49dc1 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts @@ -7,7 +7,6 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { RuleMigrationResource } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import { UpsertRuleMigrationResourcesRequestBody, UpsertRuleMigrationResourcesRequestParams, @@ -15,6 +14,7 @@ import { } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATION_RESOURCES_PATH } from '../../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import type { CreateRuleMigrationResourceInput } from '../../data/rule_migrations_data_resources_client'; import { withLicense } from '../util/with_license'; export const registerSiemRuleMigrationsResourceUpsertRoute = ( @@ -49,7 +49,7 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const ruleMigrations = resources.map((resource) => ({ + const ruleMigrations = resources.map((resource) => ({ migration_id: migrationId, ...resource, })); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts index 66b463da79cc3..5cef58cbae339 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts @@ -14,23 +14,35 @@ import type { import type { StoredRuleMigrationResource } from '../types'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; +export type CreateRuleMigrationResourceInput = Omit; + /* BULK_MAX_SIZE defines the number to break down the bulk operations by. * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. */ const BULK_MAX_SIZE = 500 as const; export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseClient { - public async upsert(resources: RuleMigrationResource[]): Promise { + public async upsert(resources: CreateRuleMigrationResourceInput[]): Promise { const index = await this.getIndexName(); - let resourcesSlice: RuleMigrationResource[]; + let resourcesSlice: CreateRuleMigrationResourceInput[]; + + const createdAt = new Date().toISOString(); + const dateFields = { + '@timestamp': createdAt, + updated_by: this.username, + updated_at: createdAt, + }; while ((resourcesSlice = resources.splice(0, BULK_MAX_SIZE)).length > 0) { await this.esClient .bulk({ refresh: 'wait_for', operations: resourcesSlice.flatMap((resource) => [ { update: { _id: this.createId(resource), _index: index } }, - { doc: resource, doc_as_upsert: true }, + { + doc: { ...resource, ...dateFields }, + doc_as_upsert: true, + }, ]), }) .catch((error) => { @@ -65,7 +77,7 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli }); } - private createId(resource: RuleMigrationResource): string { + private createId(resource: CreateRuleMigrationResourceInput): string { const key = `${resource.migration_id}-${resource.type}-${resource.name}`; return sha256.create().update(key).hex(); } From 94c56c85e644ddaa547dfad9cccaf5f8df59f8bb Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 28 Nov 2024 14:50:30 +0100 Subject: [PATCH 06/48] add connector selection and auto-resume stopped migrations within the request polling --- .../components/hooks/use_stored_state.ts | 2 +- .../common/connectors/connector_cards.tsx | 121 +++++++++++------- .../connectors/create_connector_popover.tsx | 2 +- .../ai_connector/ai_connector_card.tsx | 23 +++- .../ai_connector/connectors_check_complete.ts | 10 +- .../ai_connector/translations.ts | 2 +- .../public/plugin_services.ts | 2 +- .../public/siem_migrations/rules/api/api.ts | 38 ++++-- .../rules/service/rule_migrations_service.ts | 58 ++++++--- .../siem_migrations/rules/service/storage.ts | 29 +++++ .../public/siem_migrations/service/index.ts | 8 +- .../service/siem_migrations_service.ts | 5 +- .../plugins/security_solution/public/types.ts | 2 +- .../rule_migrations_data_resources_client.ts | 12 +- 14 files changed, 212 insertions(+), 102 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts index 70317d90c933d..b463e2551bdb8 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts @@ -13,7 +13,7 @@ import type { IntegrationTabId } from '../onboarding_body/cards/integrations/typ /** * Wrapper hook for useLocalStorage, but always returns the default value when not defined instead of `undefined`. */ -const useDefinedLocalStorage = (key: string, defaultValue: T) => { +export const useDefinedLocalStorage = (key: string, defaultValue: T) => { const [value, setValue] = useLocalStorage(key, defaultValue); return useMemo(() => [value ?? defaultValue, setValue] as const, [value, defaultValue, setValue]); }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx index 8a4f0b648b5c9..b8b51198c75ff 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { type AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector'; import { EuiFlexGroup, @@ -16,20 +16,22 @@ import { EuiBadge, EuiSpacer, EuiCallOut, + useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/css'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { CreateConnectorPopover } from './create_connector_popover'; +import { + CreateConnectorPopover, + type CreateConnectorPopoverProps, +} from './create_connector_popover'; import { ConnectorSetup } from './connector_setup'; import * as i18n from './translations'; import { MissingPrivilegesDescription } from './missing_privileges'; -interface ConnectorCardsProps { - connectors?: AIConnector[]; - onConnectorSaved: () => void; - canCreateConnectors?: boolean; - selectedConnectorId?: string; - setSelectedConnectorId?: (id: string) => void; +interface ConnectorCardsProps + extends CreateConnectorPopoverProps, + Omit { + connectors?: AIConnector[]; // make connectors optional to handle loading state } export const ConnectorCards = React.memo( @@ -40,10 +42,6 @@ export const ConnectorCards = React.memo( selectedConnectorId, setSelectedConnectorId, }) => { - const { - triggersActionsUi: { actionTypeRegistry }, - } = useKibana().services; - if (!connectors) { return ; } @@ -63,7 +61,11 @@ export const ConnectorCards = React.memo( <> {hasConnectors ? ( <> - + ['services']['triggersActionsUi']['actionTypeRegistry']; + selectedConnectorId?: string | null; + setSelectedConnectorId?: (id: string) => void; } -const ConnectorList = React.memo(({ connectors, actionTypeRegistry }) => ( - - {connectors.map((connector) => ( - ( + ({ connectors, selectedConnectorId, setSelectedConnectorId }) => { + const { euiTheme } = useEuiTheme(); + const { actionTypeRegistry } = useKibana().services.triggersActionsUi; + const onConnectorClick = useCallback( + (id: string) => { + setSelectedConnectorId?.(id); + }, + [setSelectedConnectorId] + ); + + const selectedCss = `border: 2px solid ${euiTheme.colors.primary};`; + + return ( + - - - ( + + onConnectorClick(connector.id) : undefined} + css={css` + ${selectedConnectorId === connector.id ? selectedCss : ''} `} + color={selectedConnectorId === connector.id ? 'primary' : 'plain'} > - {connector.name} - - - - {actionTypeRegistry.get(connector.actionTypeId).actionTypeTitle} - - - - - - ))} - -)); + + + {connector.name} + + + + {actionTypeRegistry.get(connector.actionTypeId).actionTypeTitle} + + + + + + ))} + + ); + } +); ConnectorList.displayName = 'ConnectorList'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx index 14d25641b91db..c6c378fc8e29f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx @@ -11,7 +11,7 @@ import { ConnectorSetup } from './connector_setup'; import * as i18n from './translations'; import { MissingPrivilegesTooltip } from './missing_privileges'; -interface CreateConnectorPopoverProps { +export interface CreateConnectorPopoverProps { onConnectorSaved: () => void; canCreateConnectors?: boolean; } diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index 4a2413a5c076b..127e6b4d57ebd 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -13,24 +13,35 @@ import { useEuiTheme, COLOR_MODES_STANDARD, } from '@elastic/eui'; -import { useStoredSiemMigrationsConnectorId } from '../../../../hooks/use_stored_state'; +import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; +import { useDefinedLocalStorage } from '../../../../hooks/use_stored_state'; import type { OnboardingCardComponent } from '../../../../../types'; import * as i18n from './translations'; import { OnboardingCardContentPanel } from '../../common/card_content_panel'; import { ConnectorCards } from '../../common/connectors/connector_cards'; import type { AIConnectorCardMetadata } from './types'; import { MissingPrivilegesCallOut } from '../../common/connectors/missing_privileges'; -import { useOnboardingContext } from '../../../../onboarding_context'; export const AIConnectorCard: OnboardingCardComponent = ({ checkCompleteMetadata, checkComplete, + setComplete, }) => { + const { siemMigrations } = useKibana().services; const { euiTheme, colorMode } = useEuiTheme(); const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; - const { spaceId } = useOnboardingContext(); - const [selectedConnectorId, setSelectedConnectorId] = useStoredSiemMigrationsConnectorId(spaceId); + const [storedConnectorId, setStoredConnectorId] = useDefinedLocalStorage( + siemMigrations.rules.connectorIdStorage.key, + null + ); + const setSelectedConnectorId = useCallback( + (connectorId: string) => { + setStoredConnectorId(connectorId); + setComplete(true); + }, + [setComplete, setStoredConnectorId] + ); const connectors = checkCompleteMetadata?.connectors; const canExecuteConnectors = checkCompleteMetadata?.canExecuteConnectors; @@ -55,7 +66,7 @@ export const AIConnectorCard: OnboardingCardComponent = canCreateConnectors={canCreateConnectors} connectors={connectors} onConnectorSaved={checkComplete} - selectedConnectorId={selectedConnectorId} + selectedConnectorId={storedConnectorId} setSelectedConnectorId={setSelectedConnectorId} /> diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts index 233d46b9911a2..d7121fe97cf7c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts @@ -7,17 +7,13 @@ import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; import type { AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { LocalStorageKey } from '../../../../../constants'; import type { OnboardingCardCheckComplete } from '../../../../../types'; import { AIActionTypeIds } from '../../common/connectors/constants'; import type { AIConnectorCardMetadata } from './types'; -const storage = new Storage(localStorage); - export const checkAssistantCardComplete: OnboardingCardCheckComplete< AIConnectorCardMetadata -> = async ({ http, application }) => { +> = async ({ http, application, siemMigrations }) => { let isComplete = false; const allConnectors = await loadConnectors({ http }); const { capabilities } = application; @@ -29,10 +25,10 @@ export const checkAssistantCardComplete: OnboardingCardCheckComplete< return acc; }, []); - const storedConnectorId = storage.get(LocalStorageKey.siemMigrationsConnectorId); + const storedConnectorId = siemMigrations.rules.connectorIdStorage.get(); if (storedConnectorId) { if (aiConnectors.length === 0) { - storage.remove(LocalStorageKey.siemMigrationsConnectorId); + siemMigrations.rules.connectorIdStorage.remove(); } else { isComplete = aiConnectors.some((connector) => connector.id === storedConnectorId); } diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts index baca7fdfe09ae..c05951e1ddf27 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/translations.ts @@ -18,6 +18,6 @@ export const AI_CONNECTOR_CARD_DESCRIPTION = i18n.translate( 'xpack.securitySolution.onboarding.aiConnector.description', { defaultMessage: - 'Choose and configure any AI provider available to use start a SIEM rules migration.', + 'Choose and configure any AI provider available to start a SIEM rules migration.', } ); diff --git a/x-pack/plugins/security_solution/public/plugin_services.ts b/x-pack/plugins/security_solution/public/plugin_services.ts index 92b4bc586a5b6..cd066da31f549 100644 --- a/x-pack/plugins/security_solution/public/plugin_services.ts +++ b/x-pack/plugins/security_solution/public/plugin_services.ts @@ -153,7 +153,7 @@ export class PluginServices { customDataService, timelineDataService, topValuesPopover: new TopValuesPopoverService(), - siemMigrations: await createSiemMigrationsService(coreStart), + siemMigrations: await createSiemMigrationsService(coreStart, startPlugins), ...(params && { onAppLeave: params.onAppLeave, setHeaderActionMenu: params.setHeaderActionMenu, diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts index 7232cb722bd1a..f953a53c281f5 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -12,10 +12,12 @@ import { KibanaServices } from '../../../common/lib/kibana'; import { SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, SIEM_RULE_MIGRATION_PATH, + SIEM_RULE_MIGRATION_START_PATH, } from '../../../../common/siem_migrations/constants'; import type { GetAllStatsRuleMigrationResponse, GetRuleMigrationResponse, + StartRuleMigrationRequestBody, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; /** @@ -32,11 +34,31 @@ export const getRuleMigrationsStatsAll = async ({ }): Promise => { return KibanaServices.get().http.fetch( SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, - { - method: 'GET', - version: '1', - signal, - } + { method: 'GET', version: '1', signal } + ); +}; + +/** + * Starts a new migration with the provided rules. + * + * @param migrationId `id` of the migration to start + * @param body The body containing the `connectorId` to use for the migration + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const startRuleMigration = async ({ + migrationId, + body, + signal, +}: { + migrationId: string; + body: StartRuleMigrationRequestBody; + signal: AbortSignal | undefined; +}): Promise => { + return KibanaServices.get().http.put( + replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }), + { body: JSON.stringify(body), version: '1', signal } ); }; @@ -57,10 +79,6 @@ export const getRuleMigrations = async ({ }): Promise => { return KibanaServices.get().http.fetch( replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), - { - method: 'GET', - version: '1', - signal, - } + { method: 'GET', version: '1', signal } ); }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index ba6543f5171d3..f155cc555d005 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -8,25 +8,31 @@ import { BehaviorSubject, type Observable } from 'rxjs'; import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import type { StartPluginsDependencies } from '../../../types'; import { ExperimentalFeaturesService } from '../../../common/experimental_features_service'; import { licenseService } from '../../../common/hooks/use_license'; -import { getRuleMigrationsStatsAll } from '../api/api'; +import { getRuleMigrationsStatsAll, startRuleMigration } from '../api/api'; import type { RuleMigrationStats } from '../types'; import { getSuccessToast } from './success_notification'; - -const POLLING_ERROR_TITLE = i18n.translate( - 'xpack.securitySolution.siemMigrations.rulesService.polling.errorTitle', - { defaultMessage: 'Error fetching rule migrations' } -); +import { RuleMigrationsStorage } from './storage'; export class SiemRulesMigrationsService { private readonly pollingInterval = 5000; private readonly latestStats$: BehaviorSubject; + private readonly signal = new AbortController().signal; private isPolling = false; + public connectorIdStorage = new RuleMigrationsStorage('connectorId'); - constructor(private readonly core: CoreStart) { + constructor( + private readonly core: CoreStart, + private readonly plugins: StartPluginsDependencies + ) { this.latestStats$ = new BehaviorSubject([]); - this.startPolling(); + + this.plugins.spaces.getActiveSpace().then((space) => { + this.connectorIdStorage.setSpaceId(space.id); + this.startPolling(); + }); } public getLatestStats$(): Observable { @@ -45,7 +51,12 @@ export class SiemRulesMigrationsService { this.isPolling = true; this.startStatsPolling() .catch((e) => { - this.core.notifications.toasts.addError(e, { title: POLLING_ERROR_TITLE }); + this.core.notifications.toasts.addError(e, { + title: i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.polling.errorTitle', + { defaultMessage: 'Error fetching rule migrations' } + ), + }); }) .finally(() => { this.isPolling = false; @@ -62,26 +73,39 @@ export class SiemRulesMigrationsService { // send notifications for finished migrations pendingMigrationIds.forEach((pendingMigrationId) => { const migration = results.find((item) => item.id === pendingMigrationId); - if (migration && migration.status === 'finished') { + if (migration?.status === 'finished') { this.core.notifications.toasts.addSuccess(getSuccessToast(migration, this.core)); } }); } - // reassign pending migrations - pendingMigrationIds = results.reduce((acc, item) => { - if (item.status === 'running') { - acc.push(item.id); + // reprocess pending migrations + pendingMigrationIds = []; + for (const result of results) { + if (result.status === 'running') { + pendingMigrationIds.push(result.id); } - return acc; - }, []); + + if (result.status === 'stopped') { + const connectorId = this.connectorIdStorage.get(); + if (connectorId) { + // automatically resume stopped migrations + await startRuleMigration({ + migrationId: result.id, + body: { connector_id: connectorId }, + signal: this.signal, + }); + pendingMigrationIds.push(result.id); + } + } + } await new Promise((resolve) => setTimeout(resolve, this.pollingInterval)); } while (pendingMigrationIds.length > 0); } private async fetchRuleMigrationsStats(): Promise { - const stats = await getRuleMigrationsStatsAll({ signal: new AbortController().signal }); + const stats = await getRuleMigrationsStatsAll({ signal: this.signal }); return stats.map((stat, index) => ({ ...stat, number: index + 1 })); // the array order (by creation) is guaranteed by the API } } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts new file mode 100644 index 0000000000000..bbf53ec3a5404 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Storage } from '@kbn/kibana-utils-plugin/public'; + +export class RuleMigrationsStorage { + private readonly storage = new Storage(localStorage); + public key: string; + + constructor(private readonly objectName: string, spaceId?: string) { + this.key = this.getStorageKey(spaceId); + } + + private getStorageKey(spaceId: string = 'default') { + return `siem_migrations.rules.${this.objectName}.${spaceId}`; + } + + public setSpaceId(spaceId: string) { + this.key = this.getStorageKey(spaceId); + } + + public get = () => this.storage.get(this.key); + public set = (value: string) => this.storage.set(this.key, value); + public remove = () => this.storage.remove(this.key); +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/service/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/service/index.ts index 08a50d018976b..dbea3624c7c1d 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/service/index.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/service/index.ts @@ -6,13 +6,17 @@ */ import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { StartPluginsDependencies } from '../../types'; export type { SiemMigrationsService } from './siem_migrations_service'; -export const createSiemMigrationsService = async (coreStart: CoreStart) => { +export const createSiemMigrationsService = async ( + coreStart: CoreStart, + plugins: StartPluginsDependencies +) => { const { SiemMigrationsService } = await import( /* webpackChunkName: "lazySiemMigrationsService" */ './siem_migrations_service' ); - return new SiemMigrationsService(coreStart); + return new SiemMigrationsService(coreStart, plugins); }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/service/siem_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/service/siem_migrations_service.ts index 1775296f6e230..da733bf5926e3 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/service/siem_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/service/siem_migrations_service.ts @@ -6,12 +6,13 @@ */ import type { CoreStart } from '@kbn/core/public'; +import type { StartPluginsDependencies } from '../../types'; import { SiemRulesMigrationsService } from '../rules/service/rule_migrations_service'; export class SiemMigrationsService { public rules: SiemRulesMigrationsService; - constructor(coreStart: CoreStart) { - this.rules = new SiemRulesMigrationsService(coreStart); + constructor(coreStart: CoreStart, plugins: StartPluginsDependencies) { + this.rules = new SiemRulesMigrationsService(coreStart, plugins); } } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index d0387c5d3abe0..f4c3cdfc0e4c6 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -138,7 +138,7 @@ export interface StartPlugins { uiActions: UiActionsStart; maps: MapsStartApi; ml?: MlPluginStart; - spaces?: SpacesPluginStart; + spaces: SpacesPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; osquery: OsqueryPluginStart; security: SecurityPluginStart; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts index 5cef58cbae339..888a41aca944c 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts @@ -28,11 +28,6 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli let resourcesSlice: CreateRuleMigrationResourceInput[]; const createdAt = new Date().toISOString(); - const dateFields = { - '@timestamp': createdAt, - updated_by: this.username, - updated_at: createdAt, - }; while ((resourcesSlice = resources.splice(0, BULK_MAX_SIZE)).length > 0) { await this.esClient .bulk({ @@ -40,7 +35,12 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli operations: resourcesSlice.flatMap((resource) => [ { update: { _id: this.createId(resource), _index: index } }, { - doc: { ...resource, ...dateFields }, + doc: { + ...resource, + '@timestamp': createdAt, + updated_by: this.username, + updated_at: createdAt, + }, doc_as_upsert: true, }, ]), From cee50d7a5ba5ec6d63ba10a00eacf83b1e6a3eeb Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 28 Nov 2024 19:57:02 +0100 Subject: [PATCH 07/48] initial implementation --- .../components/onboarding_body/body_config.ts | 7 +++ .../ai_connector/ai_connector_card.tsx | 9 +--- .../ai_connector/connectors_check_complete.ts | 2 +- .../siem_migrations/ai_connector/index.ts | 4 +- .../images/card_header_icon.png | Bin 0 -> 1251 bytes .../siem_migrations/start_migration/index.ts | 29 +++++++++++ .../start_migration/start_migration_card.tsx | 49 ++++++++++++++++++ .../start_migration_check_complete.ts | 18 +++++++ .../start_migration/translations.ts | 15 ++++++ .../siem_migrations/start_migration/types.ts | 12 +++++ .../public/onboarding/constants.ts | 1 + .../siem_migrations/common/icon/index.tsx | 8 +++ .../common/icon/siem_migrations.svg | 28 ++++++++++ .../public/siem_migrations/rules/api/api.ts | 8 +-- .../rules/service/rule_migrations_service.ts | 4 +- 15 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/images/card_header_icon.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/common/icon/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts index 93690f98b48e8..59d11314171a6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts @@ -13,6 +13,7 @@ import { rulesCardConfig } from './cards/rules'; import { alertsCardConfig } from './cards/alerts'; import { assistantCardConfig } from './cards/assistant'; import { aiConnectorCardConfig } from './cards/siem_migrations/ai_connector'; +import { startMigrationCardConfig } from './cards/siem_migrations/start_migration'; export const defaultBodyConfig: OnboardingGroupConfig[] = [ { @@ -43,4 +44,10 @@ export const siemMigrationsBodyConfig: OnboardingGroupConfig[] = [ }), cards: [aiConnectorCardConfig], }, + { + title: i18n.translate('xpack.securitySolution.onboarding.migrate.title', { + defaultMessage: 'Migrate rules & add data', + }), + cards: [startMigrationCardConfig], + }, ]; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index 127e6b4d57ebd..61760d035ce16 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx @@ -28,7 +28,7 @@ export const AIConnectorCard: OnboardingCardComponent = setComplete, }) => { const { siemMigrations } = useKibana().services; - const { euiTheme, colorMode } = useEuiTheme(); + const { colorMode } = useEuiTheme(); const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; const [storedConnectorId, setStoredConnectorId] = useDefinedLocalStorage( @@ -48,12 +48,7 @@ export const AIConnectorCard: OnboardingCardComponent = const canCreateConnectors = checkCompleteMetadata?.canCreateConnectors; return ( - + {canExecuteConnectors ? ( diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts index d7121fe97cf7c..e37b3ada95575 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts @@ -11,7 +11,7 @@ import type { OnboardingCardCheckComplete } from '../../../../../types'; import { AIActionTypeIds } from '../../common/connectors/constants'; import type { AIConnectorCardMetadata } from './types'; -export const checkAssistantCardComplete: OnboardingCardCheckComplete< +export const checkAiConnectorsCardComplete: OnboardingCardCheckComplete< AIConnectorCardMetadata > = async ({ http, application, siemMigrations }) => { let isComplete = false; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts index 45080123889d5..defa83eda98e6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts @@ -10,7 +10,7 @@ import { AssistantAvatar } from '@kbn/elastic-assistant'; import type { OnboardingCardConfig } from '../../../../../types'; import { OnboardingCardId } from '../../../../../constants'; import { AI_CONNECTOR_CARD_TITLE } from './translations'; -import { checkAssistantCardComplete } from './connectors_check_complete'; +import { checkAiConnectorsCardComplete } from './connectors_check_complete'; import type { AIConnectorCardMetadata } from './types'; export const aiConnectorCardConfig: OnboardingCardConfig = { @@ -24,6 +24,6 @@ export const aiConnectorCardConfig: OnboardingCardConfigGZx^prwfgF}}M_)$E)e-c@N+zL+@$B+ufw{znCq8vqzo#)w-S+mJ{f%OH^ z4(<$~ke$5vK{HX@K3 zuds_=_^he=`P&X13%_5lvov&9;ClI(!z~7aOi!+v8*47*W6awW_+8Wg-QC9R+b8`G zsa8DlPKCpzqaOTzH zfz@{{UpR#^e15{sm?|M?%>LA$$&>NSEdB>m6&1gzC#K(>KUq$N)uZuNXGGbpUn}p4 zI_b@F&i||&oE7K(v--%C&E@~p{P>HQe)#VXYJCt`_}A&g&&yR?YohEkzOJ7qQ@h({ zbvU=I;{6%bP4TNYsr9lZO%PVl-4k#;9Ov{ zQN%_mtMF5ma#P%*Z_Llbk{CN23-|+y9m7+#w(@VBxOT!$V+WqD*#!@*YKy)+6-xT_ z{oe#RVflH1qfjB{*sQ&DVx_|3)i-%-8IqeM zFIfe!t}l*{I4QonUh{m3t>UBqzPFw=uyHsU@qS$$*>Hp@?b)I#E4OWY7Pjlce69<- z9XXTujA_vXy(2!?gYEa;Hi-Fm&uqPEF+<{s@I%jDUd?^>|LgDh3qJpyc7L1NpC{jE za)i4q-d!hNGAr`i?W+ z5g0&;j0n{E$Eb6kwB?zFYD>2FK61OBl`6q)L zJ^e3aQ{q|gr{6j8ue@g4l?%8%}rcAO}inZM(Q}a&?ZuIq+ob9#I*!EaC zwpf|J@9vKozBe?@KM6$i@&jEvaox^|?8k~~mSpEWY1}9y|8kaeWkAkx_g&B2n)7Iojc`SCjMhJ!QCGXvf9opt9EH#xT1 zA3t6Wn11KP;RItl{?Z2RyeEkpMdz!x&fv~_61YKde)`l8TXs)$SAF@z<#tesl = { + id: OnboardingCardId.siemMigrationsStart, + title: START_MIGRATION_CARD_TITLE, + icon: cardIcon, + Component: React.lazy( + () => + import( + /* webpackChunkName: "onboarding_siem_migrations_upload_rules_card" */ + './start_migration_card' + ) + ), + checkComplete: checkStartMigrationCardComplete, + licenseTypeRequired: 'enterprise', +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx new file mode 100644 index 0000000000000..3684f10f3ce81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme, EuiIcon, EuiButton } from '@elastic/eui'; +import { SiemMigrationsIcon } from '../../../../../../siem_migrations/common/icon'; +import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; +import type { OnboardingCardComponent } from '../../../../../types'; +import { OnboardingCardContentPanel } from '../../common/card_content_panel'; +import type { StartMigrationCardMetadata } from './types'; +// import * as i18n from './translations'; + +export const UploadRulesCard: OnboardingCardComponent = ({ + checkCompleteMetadata, + checkComplete, + setComplete, +}) => { + const { siemMigrations } = useKibana().services; + const { euiTheme } = useEuiTheme(); + const stats = checkCompleteMetadata?.migrationsStats; + + return ( + + + + + + + +

{'todo'}

+
+
+ + {'todo: UPLOAD RULES'} + +
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default UploadRulesCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts new file mode 100644 index 0000000000000..0cdf89827bced --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts @@ -0,0 +1,18 @@ +/* + * 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 { getRuleMigrationsStatsAll } from '../../../../../../siem_migrations/rules/api/api'; +import type { OnboardingCardCheckComplete } from '../../../../../types'; +import type { StartMigrationCardMetadata } from './types'; + +export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< + StartMigrationCardMetadata +> = async () => { + const migrationsStats = await getRuleMigrationsStatsAll(); + const isComplete = migrationsStats.length > 0; + return { isComplete, metadata: { migrationsStats } }; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts new file mode 100644 index 0000000000000..cd2a928d0bc2f --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const START_MIGRATION_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.title', + { + defaultMessage: 'Translate your existing SIEM Rules to Elastic', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts new file mode 100644 index 0000000000000..07d6459121729 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; + +export interface StartMigrationCardMetadata { + migrationsStats: RuleMigrationTaskStats[]; +} diff --git a/x-pack/plugins/security_solution/public/onboarding/constants.ts b/x-pack/plugins/security_solution/public/onboarding/constants.ts index b1dc142965322..498f0155d6479 100644 --- a/x-pack/plugins/security_solution/public/onboarding/constants.ts +++ b/x-pack/plugins/security_solution/public/onboarding/constants.ts @@ -21,6 +21,7 @@ export enum OnboardingCardId { // siem_migrations topic cards siemMigrationsAiConnectors = 'ai_connectors', + siemMigrationsStart = 'start', } export const LocalStorageKey = { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/common/icon/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/index.tsx new file mode 100644 index 0000000000000..c0528a9a04afe --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/index.tsx @@ -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. + */ +import SiemMigrationsIconSVG from './siem_migrations.svg'; +export const SiemMigrationsIcon = SiemMigrationsIconSVG; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg new file mode 100644 index 0000000000000..6acc3183b3395 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts index f953a53c281f5..1b798bca552ae 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -30,8 +30,8 @@ import type { export const getRuleMigrationsStatsAll = async ({ signal, }: { - signal: AbortSignal | undefined; -}): Promise => { + signal?: AbortSignal; +} = {}): Promise => { return KibanaServices.get().http.fetch( SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, { method: 'GET', version: '1', signal } @@ -54,7 +54,7 @@ export const startRuleMigration = async ({ }: { migrationId: string; body: StartRuleMigrationRequestBody; - signal: AbortSignal | undefined; + signal?: AbortSignal; }): Promise => { return KibanaServices.get().http.put( replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }), @@ -75,7 +75,7 @@ export const getRuleMigrations = async ({ signal, }: { migrationId: string; - signal: AbortSignal | undefined; + signal?: AbortSignal; }): Promise => { return KibanaServices.get().http.fetch( replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index f155cc555d005..b81eaad648988 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -19,7 +19,6 @@ import { RuleMigrationsStorage } from './storage'; export class SiemRulesMigrationsService { private readonly pollingInterval = 5000; private readonly latestStats$: BehaviorSubject; - private readonly signal = new AbortController().signal; private isPolling = false; public connectorIdStorage = new RuleMigrationsStorage('connectorId'); @@ -93,7 +92,6 @@ export class SiemRulesMigrationsService { await startRuleMigration({ migrationId: result.id, body: { connector_id: connectorId }, - signal: this.signal, }); pendingMigrationIds.push(result.id); } @@ -105,7 +103,7 @@ export class SiemRulesMigrationsService { } private async fetchRuleMigrationsStats(): Promise { - const stats = await getRuleMigrationsStatsAll({ signal: this.signal }); + const stats = await getRuleMigrationsStatsAll(); return stats.map((stat, index) => ({ ...stat, number: index + 1 })); // the array order (by creation) is guaranteed by the API } } From 5a9a06a95b677ab9aff365bf51d60c8080de80d4 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 28 Nov 2024 20:10:52 +0100 Subject: [PATCH 08/48] extract common styles into single places --- .../cards/alerts/alerts_card.tsx | 23 +++------------ .../cards/assistant/assistant_card.tsx | 25 +++-------------- .../common/card_content_image_panel.styles.ts | 7 ++--- .../cards/common/card_content_panel.styles.ts | 28 +++++++++++++++++++ .../cards/common/card_content_panel.tsx | 25 +++++++---------- .../cards/common/card_subdued_text.tsx | 21 ++++++++++++++ .../cards/dashboards/dashboards_card.tsx | 23 +++------------ .../cards/rules/rules_card.tsx | 23 +++------------ .../onboarding_card_panel.styles.ts | 18 ++++++++---- 9 files changed, 89 insertions(+), 104 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_subdued_text.tsx diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx index 85d3994d44530..890505f3f618b 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx @@ -6,22 +6,14 @@ */ import React, { useCallback, useMemo } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiSpacer, - EuiText, - useEuiTheme, - COLOR_MODES_STANDARD, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import { SecuritySolutionLinkButton } from '../../../../../common/components/links'; import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel'; import { CardCallOut } from '../common/card_callout'; +import { CardSubduedText } from '../common/card_subdued_text'; import alertsImageSrc from './images/alerts.png'; import * as i18n from './translations'; @@ -30,9 +22,6 @@ export const AlertsCard: OnboardingCardComponent = ({ setExpandedCardId, setComplete, }) => { - const { colorMode } = useEuiTheme(); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; - const isIntegrationsCardComplete = useMemo( () => isCardComplete(OnboardingCardId.integrations), [isCardComplete] @@ -51,13 +40,9 @@ export const AlertsCard: OnboardingCardComponent = ({ alignItems="flexStart" > - + {i18n.ALERTS_CARD_DESCRIPTION} - + {!isIntegrationsCardComplete && ( <> diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx index b728606937020..d5ad03ae227cd 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx @@ -6,16 +6,7 @@ */ import React, { useCallback, useMemo } from 'react'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiText, - useEuiTheme, - COLOR_MODES_STANDARD, -} from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; import { css } from '@emotion/css'; import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; @@ -23,6 +14,7 @@ import * as i18n from './translations'; import { OnboardingCardContentPanel } from '../common/card_content_panel'; import { ConnectorCards } from './connectors/connector_cards'; import { CardCallOut } from '../common/card_callout'; +import { CardSubduedText } from '../common/card_subdued_text'; import type { AssistantCardMetadata } from './types'; import { MissingPrivilegesDescription } from './connectors/missing_privileges_tooltip'; @@ -32,8 +24,6 @@ export const AssistantCard: OnboardingCardComponent = ({ checkCompleteMetadata, checkComplete, }) => { - const { euiTheme, colorMode } = useEuiTheme(); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; const isIntegrationsCardComplete = useMemo( () => isCardComplete(OnboardingCardId.integrations), [isCardComplete] @@ -48,18 +38,11 @@ export const AssistantCard: OnboardingCardComponent = ({ const canCreateConnectors = checkCompleteMetadata?.canCreateConnectors; return ( - + {canExecuteConnectors ? ( - - {i18n.ASSISTANT_CARD_DESCRIPTION} - + {i18n.ASSISTANT_CARD_DESCRIPTION} {isIntegrationsCardComplete ? ( diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts index c7998135aa8ae..2260b95ab9442 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts @@ -6,15 +6,12 @@ */ import { css } from '@emotion/css'; -import { useEuiTheme, useEuiShadow, COLOR_MODES_STANDARD } from '@elastic/eui'; +import { useEuiTheme, useEuiShadow } from '@elastic/eui'; export const useCardContentImagePanelStyles = () => { - const { euiTheme, colorMode } = useEuiTheme(); + const { euiTheme } = useEuiTheme(); const shadowStyles = useEuiShadow('m'); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; return css` - padding-top: 8px; - ${isDarkMode ? `background-color: ${euiTheme.colors.lightestShade}` : ''}; .cardSpacer { width: 8%; } diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.styles.ts new file mode 100644 index 0000000000000..841fbb530775c --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.styles.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { css } from '@emotion/css'; +import { COLOR_MODES_STANDARD, useEuiTheme } from '@elastic/eui'; +import { useDarkPanelStyles } from '../../onboarding_card_panel.styles'; + +export const NESTED_PANEL_CLASS_NAME = 'onboardingCardContentPanelNested'; + +export const useCardContentPanelStyles = () => { + const { euiTheme, colorMode } = useEuiTheme(); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + const darkModeStyles = useDarkPanelStyles(isDarkMode); + + return css` + padding-top: 0; + ${darkModeStyles} + + .${NESTED_PANEL_CLASS_NAME} { + padding-top: ${euiTheme.size.s}; + ${darkModeStyles} + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx index 3d5489b9be1cc..be1b01fc77081 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx @@ -5,24 +5,19 @@ * 2.0. */ import React, { type PropsWithChildren } from 'react'; -import { COLOR_MODES_STANDARD, EuiPanel, useEuiTheme, type EuiPanelProps } from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiPanel, type EuiPanelProps } from '@elastic/eui'; +import classnames from 'classnames'; +import { useCardContentPanelStyles, NESTED_PANEL_CLASS_NAME } from './card_content_panel.styles'; export const OnboardingCardContentPanel = React.memo>( - ({ children, ...panelProps }) => { - const { euiTheme, colorMode } = useEuiTheme(); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + ({ children, className, ...panelProps }) => { + const styles = useCardContentPanelStyles(); + const panelClassName = classnames(styles); + const nestedClassName = classnames(NESTED_PANEL_CLASS_NAME, className); + return ( - - + + {children} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_subdued_text.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_subdued_text.tsx new file mode 100644 index 0000000000000..6e022b639204e --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_subdued_text.tsx @@ -0,0 +1,21 @@ +/* + * 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 { EuiText, useEuiTheme, COLOR_MODES_STANDARD, type EuiTextProps } from '@elastic/eui'; + +export type CardSubduedTextProps = PropsWithChildren; +export const CardSubduedText = React.memo(({ children, ...props }) => { + const { colorMode } = useEuiTheme(); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + return ( + + {children} + + ); +}); +CardSubduedText.displayName = 'CardSubduedText'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx index 201aa4f0d3150..94449cce5ad0e 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx @@ -6,22 +6,14 @@ */ import React, { useCallback, useMemo } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiSpacer, - EuiText, - useEuiTheme, - COLOR_MODES_STANDARD, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel'; import { CardCallOut } from '../common/card_callout'; import { CardLinkButton } from '../common/card_link_button'; +import { CardSubduedText } from '../common/card_subdued_text'; import dashboardsImageSrc from './images/dashboards.png'; import * as i18n from './translations'; @@ -30,9 +22,6 @@ export const DashboardsCard: OnboardingCardComponent = ({ setComplete, setExpandedCardId, }) => { - const { colorMode } = useEuiTheme(); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; - const isIntegrationsCardComplete = useMemo( () => isCardComplete(OnboardingCardId.integrations), [isCardComplete] @@ -54,13 +43,9 @@ export const DashboardsCard: OnboardingCardComponent = ({ alignItems="flexStart" > - + {i18n.DASHBOARDS_CARD_DESCRIPTION} - + {!isIntegrationsCardComplete && ( <> diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx index 50c722d49c359..1fc74109ca824 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx @@ -6,29 +6,18 @@ */ import React, { useCallback, useMemo } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiSpacer, - EuiText, - useEuiTheme, - COLOR_MODES_STANDARD, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import { SecuritySolutionLinkButton } from '../../../../../common/components/links'; import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel'; import { CardCallOut } from '../common/card_callout'; +import { CardSubduedText } from '../common/card_subdued_text'; import rulesImageSrc from './images/rules.png'; import * as i18n from './translations'; export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpandedCardId }) => { - const { colorMode } = useEuiTheme(); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; - const isIntegrationsCardComplete = useMemo( () => isCardComplete(OnboardingCardId.integrations), [isCardComplete] @@ -47,13 +36,9 @@ export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpanded alignItems="flexStart" > - + {i18n.RULES_CARD_DESCRIPTION} - + {!isIntegrationsCardComplete && ( <> diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts index a01d80b27488f..b703645650614 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts @@ -14,6 +14,7 @@ export const useCardPanelStyles = () => { const { euiTheme, colorMode } = useEuiTheme(); const successBackgroundColor = useEuiBackgroundColor('success'); const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + const darkModeStyles = useDarkPanelStyles(isDarkMode); return css` .onboardingCardHeader { @@ -57,11 +58,16 @@ export const useCardPanelStyles = () => { background-color: ${successBackgroundColor}; } } - ${isDarkMode - ? ` - background-color: ${euiTheme.colors.lightestShade}; - border: 1px solid ${euiTheme.colors.mediumShade}; - ` - : ''} + ${darkModeStyles} `; }; + +export const useDarkPanelStyles = (isDarkMode: boolean) => { + const { euiTheme } = useEuiTheme(); + return isDarkMode + ? ` + background-color: ${euiTheme.colors.lightestShade}; + border-color: ${euiTheme.colors.mediumShade}; + ` + : ''; +}; From f2beaf3c86731fdac2e1eebcb72c1662a7839c4e Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 28 Nov 2024 20:15:30 +0100 Subject: [PATCH 09/48] format code --- .../cards/common/card_content_panel.styles.ts | 7 +++---- .../onboarding_body/onboarding_card_panel.styles.ts | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.styles.ts index 841fbb530775c..7d0fcf831e4e4 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.styles.ts @@ -13,16 +13,15 @@ export const NESTED_PANEL_CLASS_NAME = 'onboardingCardContentPanelNested'; export const useCardContentPanelStyles = () => { const { euiTheme, colorMode } = useEuiTheme(); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; - const darkModeStyles = useDarkPanelStyles(isDarkMode); + const darkPanelStyles = useDarkPanelStyles(colorMode === COLOR_MODES_STANDARD.dark); return css` padding-top: 0; - ${darkModeStyles} + ${darkPanelStyles} .${NESTED_PANEL_CLASS_NAME} { padding-top: ${euiTheme.size.s}; - ${darkModeStyles} + ${darkPanelStyles} } `; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts index b703645650614..ca3e3e765b977 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts @@ -64,10 +64,9 @@ export const useCardPanelStyles = () => { export const useDarkPanelStyles = (isDarkMode: boolean) => { const { euiTheme } = useEuiTheme(); - return isDarkMode - ? ` + const darkPanelStyles = css` background-color: ${euiTheme.colors.lightestShade}; border-color: ${euiTheme.colors.mediumShade}; - ` - : ''; + `; + return isDarkMode ? darkPanelStyles : ''; }; From 8b60935e18fc700db361cc0bda7c023b36197bdb Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 29 Nov 2024 11:45:51 +0100 Subject: [PATCH 10/48] cleaning --- .../common/siem_migrations/constants.ts | 7 ++++++ .../api/rules/rule_migration.schema.yaml | 9 +++++++ .../model/rule_migration.gen.ts | 12 ++++++++-- .../model/rule_migration.schema.yaml | 18 ++++++++------ .../components/hooks/use_stored_state.ts | 24 +++++++++---------- .../public/onboarding/constants.ts | 11 --------- .../rules/service/rule_migrations_service.ts | 21 ++++++++-------- .../rules/service/success_notification.tsx | 6 ++--- .../public/siem_migrations/rules/types.ts | 2 +- .../rules/task/rule_migrations_task_client.ts | 15 +++++++----- 10 files changed, 73 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts index 8a2d6cf3775c9..5e6c5edbfd3fc 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts @@ -17,6 +17,13 @@ export const SIEM_RULE_MIGRATION_STOP_PATH = `${SIEM_RULE_MIGRATION_PATH}/stop` export const SIEM_RULE_MIGRATION_RESOURCES_PATH = `${SIEM_RULE_MIGRATION_PATH}/resources` as const; +export enum SiemMigrationTaskStatus { + READY = 'ready', + RUNNING = 'running', + STOPPED = 'stopped', + FINISHED = 'finished', +} + export enum SiemMigrationStatus { PENDING = 'pending', PROCESSING = 'processing', diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index b7f5f79fd2b82..6e713e498f6be 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -10,6 +10,7 @@ paths: summary: Creates a new rule migration operationId: CreateRuleMigration x-codegen-enabled: true + x-internal: true description: Creates a new SIEM rules migration using the original vendor rules provided tags: - SIEM Rule Migrations @@ -39,6 +40,7 @@ paths: summary: Updates rules migrations operationId: UpdateRuleMigration x-codegen-enabled: true + x-internal: true description: Updates rules migrations attributes tags: - SIEM Rule Migrations @@ -84,6 +86,7 @@ paths: summary: Retrieves the stats for all rule migrations operationId: GetAllStatsRuleMigration x-codegen-enabled: true + x-internal: true description: Retrieves the rule migrations stats for all migrations stored in the system tags: - SIEM Rule Migrations @@ -104,6 +107,7 @@ paths: summary: Retrieves all the rules of a migration operationId: GetRuleMigration x-codegen-enabled: true + x-internal: true description: Retrieves the rule documents stored in the system given the rule migration id tags: - SIEM Rule Migrations @@ -131,6 +135,7 @@ paths: summary: Starts a rule migration operationId: StartRuleMigration x-codegen-enabled: true + x-internal: true description: Starts a SIEM rules migration using the migration id provided tags: - SIEM Rule Migrations @@ -175,6 +180,7 @@ paths: summary: Gets a rule migration task stats operationId: GetRuleMigrationStats x-codegen-enabled: true + x-internal: true description: Retrieves the stats of a SIEM rules migration using the migration id provided tags: - SIEM Rule Migrations @@ -200,6 +206,7 @@ paths: summary: Stops an existing rule migration operationId: StopRuleMigration x-codegen-enabled: true + x-internal: true description: Stops a running SIEM rules migration using the migration id provided tags: - SIEM Rule Migrations @@ -233,6 +240,7 @@ paths: summary: Creates or updates rule migration resources for a migration operationId: UpsertRuleMigrationResources x-codegen-enabled: true + x-internal: true description: Creates or updates resources for an existing SIEM rules migration tags: - SIEM Rule Migrations @@ -270,6 +278,7 @@ paths: summary: Gets rule migration resources for a migration operationId: GetRuleMigrationResources x-codegen-enabled: true + x-internal: true description: Retrieves resources for an existing SIEM rules migration tags: - SIEM Rule Migrations diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 2260b83190e22..82e3c5549fd86 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -113,7 +113,7 @@ export type RuleMigrationTranslationResultEnum = typeof RuleMigrationTranslation export const RuleMigrationTranslationResultEnum = RuleMigrationTranslationResult.enum; /** - * The status of the rule migration process. + * The status of each rule migration. */ export type RuleMigrationStatus = z.infer; export const RuleMigrationStatus = z.enum(['pending', 'processing', 'completed', 'failed']); @@ -186,6 +186,14 @@ export const RuleMigration = z }) .merge(RuleMigrationData); +/** + * The status of the migration task. + */ +export type RuleMigrationTaskStatus = z.infer; +export const RuleMigrationTaskStatus = z.enum(['ready', 'running', 'stopped', 'finished']); +export type RuleMigrationTaskStatusEnum = typeof RuleMigrationTaskStatus.enum; +export const RuleMigrationTaskStatusEnum = RuleMigrationTaskStatus.enum; + /** * The rule migration task stats object. */ @@ -198,7 +206,7 @@ export const RuleMigrationTaskStats = z.object({ /** * Indicates if the migration task status. */ - status: z.enum(['ready', 'running', 'stopped', 'finished']), + status: RuleMigrationTaskStatus, /** * The rules migration stats. */ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 17c70665b9ad3..82892b4fa0722 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -155,13 +155,8 @@ components: description: The migration id $ref: './common.schema.yaml#/components/schemas/NonEmptyString' status: - type: string description: Indicates if the migration task status. - enum: - - ready - - running - - stopped - - finished + $ref: '#/components/schemas/RuleMigrationTaskStatus' rules: type: object description: The rules migration stats. @@ -194,6 +189,15 @@ components: type: string description: The moment of the last update. + RuleMigrationTaskStatus: + type: string + description: The status of the migration task. + enum: # should match SiemMigrationTaskStatus enum at ../constants.ts + - ready + - running + - stopped + - finished + RuleMigrationTranslationResult: type: string description: The rule translation result. @@ -204,7 +208,7 @@ components: RuleMigrationStatus: type: string - description: The status of the rule migration process. + description: The status of each rule migration. enum: # should match SiemMigrationsStatus enum at ../constants.ts - pending - processing diff --git a/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts index b463e2551bdb8..87e22de599aae 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts @@ -6,16 +6,25 @@ */ import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { useMemo } from 'react'; -import { LocalStorageKey, type OnboardingCardId } from '../../constants'; +import type { OnboardingCardId } from '../../constants'; import type { IntegrationTabId } from '../onboarding_body/cards/integrations/types'; +const LocalStorageKey = { + avcBannerDismissed: 'securitySolution.onboarding.avcBannerDismissed', + videoVisited: 'securitySolution.onboarding.videoVisited', + completeCards: 'securitySolution.onboarding.completeCards', + expandedCard: 'securitySolution.onboarding.expandedCard', + urlDetails: 'securitySolution.onboarding.urlDetails', + selectedIntegrationTabId: 'securitySolution.onboarding.selectedIntegrationTabId', + integrationSearchTerm: 'securitySolution.onboarding.integrationSearchTerm', +} as const; + /** * Wrapper hook for useLocalStorage, but always returns the default value when not defined instead of `undefined`. */ export const useDefinedLocalStorage = (key: string, defaultValue: T) => { const [value, setValue] = useLocalStorage(key, defaultValue); - return useMemo(() => [value ?? defaultValue, setValue] as const, [value, defaultValue, setValue]); + return [value ?? defaultValue, setValue] as const; }; /** @@ -56,12 +65,3 @@ export const useStoredIntegrationSearchTerm = (spaceId: string) => `${LocalStorageKey.integrationSearchTerm}.${spaceId}`, null ); - -/** - * Stores the siem migrations connector id - */ -export const useStoredSiemMigrationsConnectorId = (spaceId: string) => - useDefinedLocalStorage( - `${LocalStorageKey.siemMigrationsConnectorId}.${spaceId}`, - undefined - ); diff --git a/x-pack/plugins/security_solution/public/onboarding/constants.ts b/x-pack/plugins/security_solution/public/onboarding/constants.ts index b1dc142965322..e360e4591bb37 100644 --- a/x-pack/plugins/security_solution/public/onboarding/constants.ts +++ b/x-pack/plugins/security_solution/public/onboarding/constants.ts @@ -22,14 +22,3 @@ export enum OnboardingCardId { // siem_migrations topic cards siemMigrationsAiConnectors = 'ai_connectors', } - -export const LocalStorageKey = { - avcBannerDismissed: 'ONBOARDING_HUB.AVC_BANNER_DISMISSED', - videoVisited: 'ONBOARDING_HUB.VIDEO_VISITED', - completeCards: 'ONBOARDING_HUB.COMPLETE_CARDS', - expandedCard: 'ONBOARDING_HUB.EXPANDED_CARD', - urlDetails: 'ONBOARDING_HUB.URL_DETAILS', - selectedIntegrationTabId: 'ONBOARDING_HUB.SELECTED_INTEGRATION_TAB_ID', - integrationSearchTerm: 'ONBOARDING_HUB.INTEGRATION_SEARCH_TERM', - siemMigrationsConnectorId: 'ONBOARDING_HUB.SIEM_MIGRATIONS_CONNECTOR_ID', -} as const; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index f155cc555d005..a872d79a46027 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -8,17 +8,18 @@ import { BehaviorSubject, type Observable } from 'rxjs'; import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants'; import type { StartPluginsDependencies } from '../../../types'; import { ExperimentalFeaturesService } from '../../../common/experimental_features_service'; import { licenseService } from '../../../common/hooks/use_license'; import { getRuleMigrationsStatsAll, startRuleMigration } from '../api/api'; -import type { RuleMigrationStats } from '../types'; +import type { RuleMigrationTask } from '../types'; import { getSuccessToast } from './success_notification'; import { RuleMigrationsStorage } from './storage'; export class SiemRulesMigrationsService { private readonly pollingInterval = 5000; - private readonly latestStats$: BehaviorSubject; + private readonly latestStats$: BehaviorSubject; private readonly signal = new AbortController().signal; private isPolling = false; public connectorIdStorage = new RuleMigrationsStorage('connectorId'); @@ -27,7 +28,7 @@ export class SiemRulesMigrationsService { private readonly core: CoreStart, private readonly plugins: StartPluginsDependencies ) { - this.latestStats$ = new BehaviorSubject([]); + this.latestStats$ = new BehaviorSubject([]); this.plugins.spaces.getActiveSpace().then((space) => { this.connectorIdStorage.setSpaceId(space.id); @@ -35,7 +36,7 @@ export class SiemRulesMigrationsService { }); } - public getLatestStats$(): Observable { + public getLatestStats$(): Observable { return this.latestStats$.asObservable(); } @@ -66,14 +67,14 @@ export class SiemRulesMigrationsService { private async startStatsPolling(): Promise { let pendingMigrationIds: string[] = []; do { - const results = await this.fetchRuleMigrationsStats(); + const results = await this.fetchRuleMigrationTasksStats(); this.latestStats$.next(results); if (pendingMigrationIds.length > 0) { // send notifications for finished migrations pendingMigrationIds.forEach((pendingMigrationId) => { const migration = results.find((item) => item.id === pendingMigrationId); - if (migration?.status === 'finished') { + if (migration?.status === SiemMigrationTaskStatus.FINISHED) { this.core.notifications.toasts.addSuccess(getSuccessToast(migration, this.core)); } }); @@ -82,14 +83,14 @@ export class SiemRulesMigrationsService { // reprocess pending migrations pendingMigrationIds = []; for (const result of results) { - if (result.status === 'running') { + if (result.status === SiemMigrationTaskStatus.RUNNING) { pendingMigrationIds.push(result.id); } - if (result.status === 'stopped') { + if (result.status === SiemMigrationTaskStatus.STOPPED) { const connectorId = this.connectorIdStorage.get(); if (connectorId) { - // automatically resume stopped migrations + // automatically resume stopped migrations when connector is available await startRuleMigration({ migrationId: result.id, body: { connector_id: connectorId }, @@ -104,7 +105,7 @@ export class SiemRulesMigrationsService { } while (pendingMigrationIds.length > 0); } - private async fetchRuleMigrationsStats(): Promise { + private async fetchRuleMigrationTasksStats(): Promise { const stats = await getRuleMigrationsStatsAll({ signal: this.signal }); return stats.map((stat, index) => ({ ...stat, number: index + 1 })); // the array order (by creation) is guaranteed by the API } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx index f87755943f830..830e3c5f4a531 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx @@ -17,9 +17,9 @@ import type { ToastInput } from '@kbn/core-notifications-browser'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { RuleMigrationStats } from '../types'; +import type { RuleMigrationTask } from '../types'; -export const getSuccessToast = (migration: RuleMigrationStats, core: CoreStart): ToastInput => ({ +export const getSuccessToast = (migration: RuleMigrationTask, core: CoreStart): ToastInput => ({ color: 'success', iconType: 'check', toastLifeTimeMs: 1000 * 60 * 30, // 30 minutes @@ -34,7 +34,7 @@ export const getSuccessToast = (migration: RuleMigrationStats, core: CoreStart): ), }); -const SuccessToastContent: React.FC<{ migration: RuleMigrationStats }> = ({ migration }) => { +const SuccessToastContent: React.FC<{ migration: RuleMigrationTask }> = ({ migration }) => { const navigation = { deepLinkId: SecurityPageName.siemMigrationsRules, path: migration.id }; const { navigateTo, getAppUrl } = useNavigation(); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts index db9ca9507702f..4c704e97179c0 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts @@ -7,7 +7,7 @@ import type { RuleMigrationTaskStats } from '../../../common/siem_migrations/model/rule_migration.gen'; -export interface RuleMigrationStats extends RuleMigrationTaskStats { +export interface RuleMigrationTask extends RuleMigrationTaskStats { /** The sequential number of the migration */ number: number; } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index 56c7e8485d315..a6ea5c9040e16 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -8,7 +8,10 @@ import type { AuthenticatedUser, Logger } from '@kbn/core/server'; import { AbortError, abortSignalToPromise } from '@kbn/kibana-utils-plugin/server'; import type { RunnableConfig } from '@langchain/core/runnables'; -import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; +import { + SiemMigrationTaskStatus, + SiemMigrationStatus, +} from '../../../../../common/siem_migrations/constants'; import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationsDataClient } from '../data/rule_migrations_data_client'; import type { RuleMigrationDataStats } from '../data/rule_migrations_data_rules_client'; @@ -237,17 +240,17 @@ export class RuleMigrationsTaskClient { private getTaskStatus( migrationId: string, dataStats: RuleMigrationDataStats['rules'] - ): RuleMigrationTaskStats['status'] { + ): SiemMigrationTaskStatus { if (this.migrationsRunning.has(migrationId)) { - return 'running'; + return SiemMigrationTaskStatus.RUNNING; } if (dataStats.pending === dataStats.total) { - return 'ready'; + return SiemMigrationTaskStatus.READY; } if (dataStats.completed + dataStats.failed === dataStats.total) { - return 'finished'; + return SiemMigrationTaskStatus.FINISHED; } - return 'stopped'; + return SiemMigrationTaskStatus.STOPPED; } /** Stops one running migration */ From ef6c17d1e62a161d1fbd0cd9f710d0aab0a628d7 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 29 Nov 2024 13:48:03 +0100 Subject: [PATCH 11/48] upload panel styles --- .../cards/common/card_content_panel.tsx | 2 +- .../start_migration_card.styles.ts | 24 ++++++++ .../start_migration/start_migration_card.tsx | 56 ++++++++++++------- .../start_migration/translations.ts | 36 ++++++++++++ .../common/icon/siem_migrations.svg | 38 ++++++------- 5 files changed, 116 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx index be1b01fc77081..7f3ba00593fc0 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx @@ -17,7 +17,7 @@ export const OnboardingCardContentPanel = React.memo - + {children} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts new file mode 100644 index 0000000000000..219986c8725a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts @@ -0,0 +1,24 @@ +/* + * 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 { css } from '@emotion/css'; +import { useEuiTheme } from '@elastic/eui'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + return css` + .siemMigrationsIcon { + width: 88px; + block-size: 88px; + inline-size: 88px; + } + .siemMigrationsUploadTitle { + font-weight: ${euiTheme.font.weight.semiBold}; + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index 3684f10f3ce81..d2977969e73e3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -6,13 +6,14 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme, EuiIcon, EuiButton } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon, EuiButton, EuiPanel } from '@elastic/eui'; import { SiemMigrationsIcon } from '../../../../../../siem_migrations/common/icon'; import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; import type { OnboardingCardComponent } from '../../../../../types'; import { OnboardingCardContentPanel } from '../../common/card_content_panel'; import type { StartMigrationCardMetadata } from './types'; -// import * as i18n from './translations'; +import { useStyles } from './start_migration_card.styles'; +import * as i18n from './translations'; export const UploadRulesCard: OnboardingCardComponent = ({ checkCompleteMetadata, @@ -20,27 +21,42 @@ export const UploadRulesCard: OnboardingCardComponent { const { siemMigrations } = useKibana().services; - const { euiTheme } = useEuiTheme(); + const styles = useStyles(); const stats = checkCompleteMetadata?.migrationsStats; return ( - - - - - - - -

{'todo'}

-
-
- - {'todo: UPLOAD RULES'} - -
+ + + + + + + + + + +

{i18n.START_MIGRATION_CARD_UPLOAD_TITLE}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_DESCRIPTION}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}

+
+
+
+
+ + + {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} + + +
+
); }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index cd2a928d0bc2f..f094086bf8f8e 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -13,3 +13,39 @@ export const START_MIGRATION_CARD_TITLE = i18n.translate( defaultMessage: 'Translate your existing SIEM Rules to Elastic', } ); + +export const START_MIGRATION_CARD_UPLOAD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.upload.title', + { + defaultMessage: 'Export your Splunk® SIEM rules to start translation.', + } +); + +export const START_MIGRATION_CARD_UPLOAD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.upload.description', + { + defaultMessage: + 'Upload your rules before importing data to identify the integrations, data streams, and available details of your SIEM rules. Click “Upload Rules” to view step-by-step instructions to export and uploading the rules.', + } +); + +export const START_MIGRATION_CARD_UPLOAD_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.upload.button', + { + defaultMessage: 'Upload 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', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg index 6acc3183b3395..e8568a943f70c 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg +++ b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg @@ -1,28 +1,28 @@ - - - - + + + + - - + + - - - - - - - - + + + + + + + + - + - - + + - - + + From 5cb839db83863299459fb27f86f130d9571a118c Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 29 Nov 2024 17:15:54 +0100 Subject: [PATCH 12/48] flyout scafolding --- .../start_migration/context.tsx | 30 ++++++ .../start_migration/start_migration_card.tsx | 65 ++++--------- .../start_migration_check_complete.ts | 5 +- .../start_migration/translations.ts | 29 +++--- .../siem_migrations/start_migration/types.ts | 4 +- ...styles.ts => upload_rules_panel.styles.ts} | 10 +- .../start_migration/upload_rules_panel.tsx | 77 +++++++++++++++ .../start_migration/upload_rules_panels.tsx | 43 ++++++++ .../public/siem_migrations/rules/api/api.ts | 51 +++++++--- .../components/data_input_flyout/constants.ts | 12 +++ .../data_input_flyout/data_input_flyout.tsx | 97 +++++++++++++++++++ .../components/data_input_flyout/index.ts | 8 ++ .../rules/service/rule_migrations_service.ts | 75 ++++++++++---- .../rules/service/translations.ts | 18 ++++ .../public/siem_migrations/rules/types.ts | 2 + 15 files changed, 422 insertions(+), 104 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/{start_migration_card.styles.ts => upload_rules_panel.styles.ts} (73%) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/index.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/translations.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx new file mode 100644 index 0000000000000..bc964da73f645 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx @@ -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 React, { createContext, useContext, useMemo, type PropsWithChildren } from 'react'; + +interface StartMigrationContextValue { + setIsFlyoutOpen: React.Dispatch>; +} + +const StartMigrationContext = createContext(null); + +export const StartMigrationContextProvider: React.FC< + PropsWithChildren +> = React.memo(({ children, setIsFlyoutOpen }) => { + const value = useMemo(() => ({ setIsFlyoutOpen }), [setIsFlyoutOpen]); + 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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index d2977969e73e3..d8c2ae45fb259 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -5,61 +5,38 @@ * 2.0. */ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon, EuiButton, EuiPanel } from '@elastic/eui'; -import { SiemMigrationsIcon } from '../../../../../../siem_migrations/common/icon'; -import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; +import React, { useCallback, useState } from 'react'; +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 type { StartMigrationCardMetadata } from './types'; -import { useStyles } from './start_migration_card.styles'; -import * as i18n from './translations'; +import { UploadRulesPanels } from './upload_rules_panels'; +import { StartMigrationContextProvider } from './context'; -export const UploadRulesCard: OnboardingCardComponent = ({ +export const StartMigrationCard: OnboardingCardComponent = ({ checkCompleteMetadata, checkComplete, setComplete, }) => { - const { siemMigrations } = useKibana().services; - const styles = useStyles(); - const stats = checkCompleteMetadata?.migrationsStats; + const migrationsStats = checkCompleteMetadata?.migrationsStats; + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + + const onClose = useCallback(() => setIsFlyoutOpen(false), [setIsFlyoutOpen]); return ( - - - - - - - - - - -

{i18n.START_MIGRATION_CARD_UPLOAD_TITLE}

- -
- - -

{i18n.START_MIGRATION_CARD_UPLOAD_DESCRIPTION}

-
-
- - -

{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}

-
-
-
-
- - - {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} - - -
- -
+ + + {migrationsStats == null ? ( + + ) : ( + + )} + + {isFlyoutOpen && } + ); }; // eslint-disable-next-line import/no-default-export -export default UploadRulesCard; +export default StartMigrationCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts index 0cdf89827bced..f0a14d56b1b27 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { getRuleMigrationsStatsAll } from '../../../../../../siem_migrations/rules/api/api'; import type { OnboardingCardCheckComplete } from '../../../../../types'; import type { StartMigrationCardMetadata } from './types'; export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< StartMigrationCardMetadata -> = async () => { - const migrationsStats = await getRuleMigrationsStatsAll(); +> = async ({ siemMigrations }) => { + const migrationsStats = await siemMigrations.rules.getRuleMigrationTasksStats(); const isComplete = migrationsStats.length > 0; return { isComplete, metadata: { migrationsStats } }; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index f094086bf8f8e..7c3090d5f4da6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -9,16 +9,12 @@ import { i18n } from '@kbn/i18n'; export const START_MIGRATION_CARD_TITLE = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.title', - { - defaultMessage: 'Translate your existing SIEM Rules to Elastic', - } + { defaultMessage: 'Translate your existing SIEM Rules to Elastic' } ); export const START_MIGRATION_CARD_UPLOAD_TITLE = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.upload.title', - { - defaultMessage: 'Export your Splunk® SIEM rules to start translation.', - } + { defaultMessage: 'Export your Splunk® SIEM rules to start translation.' } ); export const START_MIGRATION_CARD_UPLOAD_DESCRIPTION = i18n.translate( @@ -31,21 +27,24 @@ export const START_MIGRATION_CARD_UPLOAD_DESCRIPTION = i18n.translate( export const START_MIGRATION_CARD_UPLOAD_BUTTON = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.upload.button', - { - defaultMessage: 'Upload Rules', - } + { defaultMessage: 'Upload rules' } +); + +export const START_MIGRATION_CARD_UPLOAD_MORE_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.uploadMore.title', + { defaultMessage: 'Need to migrate more rules?' } +); +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.', - } + { 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', - } + { defaultMessage: 'Read AI docs' } ); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts index 07d6459121729..68a8f5edf76d6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types'; export interface StartMigrationCardMetadata { - migrationsStats: RuleMigrationTaskStats[]; + migrationsStats: RuleMigrationStats[]; } diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts similarity index 73% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts index 219986c8725a0..d8444b324fa9d 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts @@ -8,14 +8,14 @@ import { css } from '@emotion/css'; import { useEuiTheme } from '@elastic/eui'; -export const useStyles = () => { +export const useStyles = (compressed: boolean) => { const { euiTheme } = useEuiTheme(); - + const logoSize = compressed ? '32px' : '88px'; return css` .siemMigrationsIcon { - width: 88px; - block-size: 88px; - inline-size: 88px; + width: ${logoSize}; + block-size: ${logoSize}; + inline-size: ${logoSize}; } .siemMigrationsUploadTitle { font-weight: ${euiTheme.font.weight.semiBold}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx new file mode 100644 index 0000000000000..4a408b9c94ab1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx @@ -0,0 +1,77 @@ +/* + * 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, + EuiIcon, + EuiButton, + EuiButtonEmpty, + EuiPanel, +} from '@elastic/eui'; +import { SiemMigrationsIcon } from '../../../../../../siem_migrations/common/icon'; +import { useStyles } from './upload_rules_panel.styles'; +import * as i18n from './translations'; +import { useStartMigrationContext } from './context'; + +export interface UploadRulesPanelProps { + isUploadMore?: boolean; +} +export const UploadRulesPanel = React.memo(({ isUploadMore = false }) => { + const styles = useStyles(isUploadMore); + const { setIsFlyoutOpen } = useStartMigrationContext(); + const openFlyout = useCallback(() => setIsFlyoutOpen(true), [setIsFlyoutOpen]); + + return ( + + + + + + + {isUploadMore ? ( + +

{i18n.START_MIGRATION_CARD_UPLOAD_MORE_TITLE}

+
+ ) : ( + + + +

{i18n.START_MIGRATION_CARD_UPLOAD_TITLE}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_DESCRIPTION}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}

+
+
+
+ )} +
+ + {isUploadMore ? ( + + {i18n.START_MIGRATION_CARD_UPLOAD_MORE_BUTTON} + + ) : ( + + {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} + + )} + +
+
+ ); +}); +UploadRulesPanel.displayName = 'UploadRulesPanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx new file mode 100644 index 0000000000000..4107ddff25dfd --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.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 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'; + +export interface UploadRulesPanelsProps { + migrationsStats: RuleMigrationStats[]; +} +export const UploadRulesPanels = React.memo(({ migrationsStats }) => { + if (migrationsStats.length === 0) { + return ; + } + + return ( + + + + + {migrationsStats.map((migrationStats) => ( + + {migrationStats.status === SiemMigrationTaskStatus.RUNNING && ( + + )} + {migrationStats.status === SiemMigrationTaskStatus.FINISHED && ( + + )} + + ))} + + ); +}); +UploadRulesPanels.displayName = 'UploadRulesPanels'; + +const RuleMigrationProcess = () => null; +const RuleMigrationResult = () => null; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts index 1b798bca552ae..51b43b077d18f 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -15,47 +15,68 @@ import { SIEM_RULE_MIGRATION_START_PATH, } from '../../../../common/siem_migrations/constants'; import type { + CreateRuleMigrationRequestBody, + CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, GetRuleMigrationResponse, StartRuleMigrationRequestBody, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +export interface GetRuleMigrationsStatsAllParams { + /** AbortSignal for cancelling request */ + signal?: AbortSignal; +} /** * Retrieves the stats for all the existing migrations, aggregated by `migration_id`. - * - * @param signal AbortSignal for cancelling request - * * @throws An error if response is not OK */ export const getRuleMigrationsStatsAll = async ({ signal, -}: { - signal?: AbortSignal; -} = {}): Promise => { +}: GetRuleMigrationsStatsAllParams = {}): Promise => { return KibanaServices.get().http.fetch( SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, { method: 'GET', version: '1', signal } ); }; +export interface CreateRuleMigrationParams { + /** The body containing the `connectorId` to use for the migration */ + body: CreateRuleMigrationRequestBody; + /** AbortSignal for cancelling request */ + signal?: AbortSignal; +} /** * Starts a new migration with the provided rules. - * - * @param migrationId `id` of the migration to start - * @param body The body containing the `connectorId` to use for the migration - * @param signal AbortSignal for cancelling request - * * @throws An error if response is not OK */ -export const startRuleMigration = async ({ - migrationId, +export const createRuleMigration = async ({ body, signal, -}: { +}: CreateRuleMigrationParams): Promise => { + return KibanaServices.get().http.post(SIEM_RULE_MIGRATION_PATH, { + body: JSON.stringify(body), + version: '1', + signal, + }); +}; + +export interface StartRuleMigrationParams { + /** `id` of the migration to start */ migrationId: string; + /** The body containing the `connectorId` to use for the migration */ body: StartRuleMigrationRequestBody; + /** AbortSignal for cancelling request */ signal?: AbortSignal; -}): Promise => { +} +/** + * Starts a new migration with the provided rules. + * @throws An error if response is not OK + */ +export const startRuleMigration = async ({ + migrationId, + body, + signal, +}: StartRuleMigrationParams): Promise => { return KibanaServices.get().http.put( replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }), { body: JSON.stringify(body), version: '1', signal } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts new file mode 100644 index 0000000000000..074bf1582e70a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum DataInputStep { + rules = 'rules', + macros = 'macros', + lookups = 'lookups', +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx new file mode 100644 index 0000000000000..0ebb53b7457eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -0,0 +1,97 @@ +/* + * 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, useState } from 'react'; +import { + EuiFlyoutResizable, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiText, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { DataInputStep } from './constants'; + +export interface MigrationDataInputFlyoutProps { + onClose: () => void; + migrationId?: string; +} + +export const MigrationDataInputFlyout = React.memo( + ({ onClose, migrationId: initialMigrationId }) => { + const { siemMigrations } = useKibana().services; + const [migrationId, setMigrationId] = useState(initialMigrationId); + + const onStart = useCallback(() => { + if (migrationId) { + siemMigrations.rules.startRuleMigration(migrationId); + onClose(); + } + }, [migrationId, siemMigrations.rules, onClose]); + + const [step, setStep] = useState( + initialMigrationId ? DataInputStep.macros : DataInputStep.rules + ); + + return ( + + + +

+ +

+
+
+ + +

+ +

+
+
+ + + + + + + + + + + + + + +
+ ); + } +); +MigrationDataInputFlyout.displayName = 'MigrationDataInputFlyout'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/index.ts new file mode 100644 index 0000000000000..709623f992f72 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/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 { MigrationDataInputFlyout, type MigrationDataInputFlyoutProps } from './data_input_flyout'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index d9c4c6cad64fd..8322457f3c3df 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -7,18 +7,26 @@ import { BehaviorSubject, type Observable } from 'rxjs'; import type { CoreStart } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; +import type { + CreateRuleMigrationRequestBody, + CreateRuleMigrationResponse, + GetAllStatsRuleMigrationResponse, + StartRuleMigrationRequestBody, +} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants'; import type { StartPluginsDependencies } from '../../../types'; import { ExperimentalFeaturesService } from '../../../common/experimental_features_service'; import { licenseService } from '../../../common/hooks/use_license'; -import { getRuleMigrationsStatsAll, startRuleMigration } from '../api/api'; +import type { GetRuleMigrationsStatsAllParams } from '../api/api'; +import { createRuleMigration, getRuleMigrationsStatsAll, startRuleMigration } from '../api/api'; import type { RuleMigrationStats } from '../types'; import { getSuccessToast } from './success_notification'; import { RuleMigrationsStorage } from './storage'; +import * as i18n from './translations'; + +const REQUEST_POLLING_INTERVAL_MS = 5000 as const; export class SiemRulesMigrationsService { - private readonly pollingInterval = 5000; private readonly latestStats$: BehaviorSubject; private isPolling = false; public connectorIdStorage = new RuleMigrationsStorage('connectorId'); @@ -47,27 +55,59 @@ export class SiemRulesMigrationsService { if (this.isPolling || !this.isAvailable()) { return; } - this.isPolling = true; - this.startStatsPolling() + this.startTaskStatsPolling() .catch((e) => { - this.core.notifications.toasts.addError(e, { - title: i18n.translate( - 'xpack.securitySolution.siemMigrations.rulesService.polling.errorTitle', - { defaultMessage: 'Error fetching rule migrations' } - ), - }); + this.core.notifications.toasts.addError(e, { title: i18n.POLLING_ERROR }); }) .finally(() => { this.isPolling = false; }); } - private async startStatsPolling(): Promise { + public async getRuleMigrationTasksStats( + params: GetRuleMigrationsStatsAllParams = {} + ): Promise { + const allStats = await getRuleMigrationsStatsAll(params); + const results = allStats.map( + (stats, index) => + ({ + ...stats, + number: index + 1, // the array order (by creation) is guaranteed by the API + } as RuleMigrationStats) // needs cast because of the `status` enum override + ); + this.latestStats$.next(results); // Always update the latest stats + return results; + } + + public async createRuleMigration( + body: CreateRuleMigrationRequestBody + ): Promise { + const connectorId = this.connectorIdStorage.get(); + if (!connectorId) { + throw new Error(i18n.MISSING_CONNECTOR_ERROR); + } + return createRuleMigration({ body }); + } + + public async startRuleMigration( + migrationId: string, + options: Pick = {} + ): Promise { + const connectorId = this.connectorIdStorage.get(); + if (!connectorId) { + throw new Error(i18n.MISSING_CONNECTOR_ERROR); + } + const body = { ...options, connector_id: connectorId }; + const result = await startRuleMigration({ migrationId, body }); + this.startPolling(); + return result; + } + + private async startTaskStatsPolling(): Promise { let pendingMigrationIds: string[] = []; do { - const results = await this.fetchRuleMigrationTasksStats(); - this.latestStats$.next(results); + const results = await this.getRuleMigrationTasksStats(); if (pendingMigrationIds.length > 0) { // send notifications for finished migrations @@ -99,12 +139,7 @@ export class SiemRulesMigrationsService { } } - await new Promise((resolve) => setTimeout(resolve, this.pollingInterval)); + await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_MS)); } while (pendingMigrationIds.length > 0); } - - private async fetchRuleMigrationTasksStats(): Promise { - const stats = await getRuleMigrationsStatsAll(); - return stats.map((stat, index) => ({ ...stat, number: index + 1 })); // the array order (by creation) is guaranteed by the API - } } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/translations.ts new file mode 100644 index 0000000000000..035b428b49333 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/translations.ts @@ -0,0 +1,18 @@ +/* + * 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 POLLING_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.pollingError', + { defaultMessage: 'Error fetching rule migrations' } +); + +export const MISSING_CONNECTOR_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.missingConnectorError', + { defaultMessage: 'Connector not defined. Please set a connector ID first.' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts index db9ca9507702f..bcc11327d1051 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts @@ -5,9 +5,11 @@ * 2.0. */ +import type { SiemMigrationTaskStatus } from '../../../common/siem_migrations/constants'; import type { RuleMigrationTaskStats } from '../../../common/siem_migrations/model/rule_migration.gen'; export interface RuleMigrationStats extends RuleMigrationTaskStats { + status: SiemMigrationTaskStatus; /** The sequential number of the migration */ number: number; } From d79dc4716921680e43f26b1fc5752df460c84208 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Sun, 1 Dec 2024 11:36:44 +0100 Subject: [PATCH 13/48] add upload input --- .../components/data_input_flyout/constants.ts | 14 ++ .../data_input_flyout/data_input_flyout.tsx | 2 +- .../steps/common/sub_step_wrapper.tsx | 26 +++ .../{ => steps/rules}/rules_data_input.tsx | 50 +++-- .../rules/sub_steps/copy_export_query.tsx | 51 +++++ .../rules/sub_steps/rules_file_upload.tsx | 179 ++++++++++++++++++ .../steps/rules/translations.ts | 67 +++++++ .../components/data_input_flyout/types.ts | 10 + .../rules/service/rule_migrations_service.ts | 1 - 9 files changed, 378 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx rename x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/{ => steps/rules}/rules_data_input.tsx (52%) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts index 074bf1582e70a..c874672399aaf 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts @@ -10,3 +10,17 @@ export enum DataInputStep { macros = 'macros', lookups = 'lookups', } + +export const SPL_RULES_COLUMNS = [ + 'id', + 'title', + 'search', + 'description', + 'action.escu.eli5', + 'action.correlationsearch.annotations', +] as const; + +export const RULES_SPL_QUERY = `| rest /servicesNS/-/Splunk_Security_Essentials/saved/searches +| union [ rest /servicesNS/-/SplunkEnterpriseSecuritySuite/saved/searches ] +| search is_scheduled=1 disabled=0 +| table ${SPL_RULES_COLUMNS.join(', ')}`; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index b3b52699486aa..15f524a56843c 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -19,7 +19,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import { DataInputStep } from './constants'; -import { RulesDataInput } from './rules_data_input'; +import { RulesDataInput } from './steps/rules/rules_data_input'; export interface MigrationDataInputFlyoutProps { onClose: () => void; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx new file mode 100644 index 0000000000000..438134b0ad99a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiPanel } from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { PropsWithChildren } from 'react'; +import React from 'react'; + +const style = css` + .euiStep__title { + font-size: 14px; + } +`; + +export const SubStepWrapper = React.memo>(({ children }) => { + return ( + + {children} + + ); +}); +SubStepWrapper.displayName = 'SubStepWrapper'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/rules_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx similarity index 52% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/rules_data_input.tsx rename to x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx index 1cf04f7e5b371..ea71684c19236 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/rules_data_input.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, @@ -13,20 +14,40 @@ import { EuiSteps, EuiTitle, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; import React, { useMemo, useState } from 'react'; +import { SubStepWrapper } from '../common/sub_step_wrapper'; +import * as i18n from './translations'; +import { CopyExportQuery } from './sub_steps/copy_export_query'; +import { RulesFileUpload } from './sub_steps/rules_file_upload'; + +type Step = 1 | 2 | 3; +const getStatus = (step: Step, currentStep: Step): EuiStepStatus => { + if (step === currentStep) { + return 'current'; + } + if (step < currentStep) { + return 'complete'; + } + return 'incomplete'; +}; export const RulesDataInput = React.memo(() => { - const [step, setStep] = useState<1 | 2 | 3>(1); + const [step, setStep] = useState(1); - const steps = useMemo( + const steps = useMemo( () => [ { title: 'Copy rule query', - children: setStep(2)} />, + status: getStatus(1, step), + children: setStep(2)} />, + }, + { + title: 'Update your rule export', + status: getStatus(2, step), + children: setStep(3)} />, }, ], - [setStep] + [setStep, step] ); return ( @@ -39,29 +60,18 @@ export const RulesDataInput = React.memo(() => { - - - + {i18n.RULES_DATA_INPUT_TITLE} - + + + ); }); RulesDataInput.displayName = 'RulesDataInput'; - -interface StepProps { - onComplete: () => void; -} -const CopyQueryStep = React.memo(({ onComplete }) => { - return

{'Do this first'}

; -}); -CopyQueryStep.displayName = 'CopyQueryStep'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query.tsx new file mode 100644 index 0000000000000..0b01985e28b3d --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query.tsx @@ -0,0 +1,51 @@ +/* + * 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 { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { SubStepProps } from '../../../types'; +import { RULES_SPL_QUERY } from '../../../constants'; +import * as i18n from '../translations'; + +export const CopyExportQuery = React.memo(({ onComplete }) => { + const onClick: React.MouseEventHandler = useCallback( + (ev) => { + // The only button inside the element is the "copy" button. + if ((ev.target as Element).tagName === 'BUTTON') { + onComplete(); + } + }, + [onComplete] + ); + + return ( + <> + {/* The click event is also dispatched when using the keyboard actions (space or enter) for "copy" button. + No need to use keyboard specific events, disabling the a11y lint rule:*/} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
+ {/* onCopy react event is dispatched when the user copies text manually */} + + {RULES_SPL_QUERY} + +
+ + + {i18n.RULES_DATA_INPUT_DESCRIPTION_SECTION}, + format: {'JSON'}, + }} + /> + + + ); +}); +CopyExportQuery.displayName = 'CopyExportQuery'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload.tsx new file mode 100644 index 0000000000000..6e5e74196aeae --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload.tsx @@ -0,0 +1,179 @@ +/* + * 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, useState } from 'react'; +import { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; +import { isPlainObject } from 'lodash/fp'; +import type { OriginalRuleAnnotations } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { OriginalRule } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from '../translations'; +import type { SubStepProps } from '../../../types'; +import type { SPL_RULES_COLUMNS } from '../../../constants'; + +type ExpectedRowFormat = Partial>; + +const parseContent = (fileContent: string): OriginalRule[] => { + const trimmedContent = fileContent.trim(); + let arrayContent: ExpectedRowFormat[]; + if (trimmedContent.startsWith('[')) { + arrayContent = parseJSONArray(trimmedContent); + } else { + arrayContent = parseNDJSON(trimmedContent); + } + if (arrayContent.length === 0) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.EMPTY); + } + return arrayContent.map(convertFormat); +}; + +const parseNDJSON = (fileContent: string): ExpectedRowFormat[] => { + return fileContent + .split(/\n(?=\{)/) // split at newline followed by '{'. + .filter((entry) => entry.trim() !== '') // Remove empty entries. + .map((entry) => JSON.parse(entry)); // Parse each entry as JSON. +}; + +const parseJSONArray = (fileContent: string): ExpectedRowFormat[] => { + let parsedContent: ExpectedRowFormat; + try { + parsedContent = JSON.parse(fileContent); + } catch (error) { + if (error instanceof RangeError) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + } + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_PARSE); + } + if (!Array.isArray(parsedContent)) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_ARRAY); + } + return parsedContent; +}; + +const convertFormat = (row: ExpectedRowFormat): OriginalRule => { + if (!isPlainObject(row)) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_OBJECT); + } + const originalRule: Partial = { + id: row.id, + vendor: 'splunk', + title: row.title, + query: row.search, + query_language: 'spl', + description: row['action.escu.eli5'] || row.description, + }; + + if (row['action.correlationsearch.annotations']) { + originalRule.annotations = processAnnotations(row['action.correlationsearch.annotations']); + } + // Validates each rule format. + const { error } = OriginalRule.safeParse(originalRule); + if (error) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.WRONG_FORMAT(error.message)); + } + return originalRule as OriginalRule; +}; + +const processAnnotations = (annotationsStr: string): OriginalRuleAnnotations | undefined => { + try { + return JSON.parse(annotationsStr); + } catch (error) { + return undefined; + } +}; + +export const RulesFileUpload = React.memo(({ onComplete }) => { + const [isParsing, setIsParsing] = useState(false); + const [fileError, setFileError] = useState(); + + const onChangeFile = useCallback((files: FileList | null) => { + if (!files) { + return; + } + + setFileError(undefined); + + const logsSampleFile = files[0]; + const reader = new FileReader(); + + reader.onloadstart = () => setIsParsing(true); + reader.onloadend = () => setIsParsing(false); + + reader.onload = function (e) { + const fileContent = e.target?.result as string | undefined; // We can safely cast to string since we call `readAsText` to load the file. + + if (fileContent == null) { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); + return; + } + + if (fileContent === '' && 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 + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + return; + } + + let data: object[]; + try { + data = parseContent(fileContent); + } catch (error) { + setFileError(error); + } + + // TODO: Create a new migration using data + + // setIntegrationSettings({ + // ...integrationSettings, + // logSamples, + // samplesFormat, + // }); + }; + + const handleReaderError = function () { + const message = reader.error?.message; + if (message) { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); + } else { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); + } + }; + + reader.onerror = handleReaderError; + reader.onabort = handleReaderError; + + reader.readAsText(logsSampleFile); + }, []); + + return ( + + {fileError} + + } + isInvalid={fileError != null} + > + + + {i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT} + + + } + onChange={onChangeFile} + display="large" + aria-label="Upload logs sample file" + isLoading={isParsing} + data-test-subj="logsSampleFilePicker" + data-loading={isParsing} + /> + + ); +}); +RulesFileUpload.displayName = 'RulesFileUpload'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts new file mode 100644 index 0000000000000..014e97f81ce05 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts @@ -0,0 +1,67 @@ +/* + * 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 RULES_DATA_INPUT_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.title', + { defaultMessage: 'Upload rule export and check for macros and lookups' } +); + +export const RULES_DATA_INPUT_DESCRIPTION_SECTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.description.section', + { defaultMessage: 'Search and Reporting' } +); + +export const RULES_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.prompt', + { defaultMessage: 'Select or drag and drop the exported JSON file' } +); + +export const RULES_DATA_INPUT_FILE_UPLOAD_ERROR = { + CAN_NOT_READ: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotRead', + { defaultMessage: 'Failed to read the rules export file' } + ), + CAN_NOT_READ_WITH_REASON: (reason: string) => + i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotReadWithReason', + { + defaultMessage: 'An error occurred when reading rules export file: {reason}', + values: { reason }, + } + ), + CAN_NOT_PARSE: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotParse', + { defaultMessage: 'Cannot parse the rules export file as either a JSON file' } + ), + TOO_LARGE_TO_PARSE: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.tooLargeToParse', + { defaultMessage: 'This rules export file is too large to parse' } + ), + NOT_ARRAY: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notArray', + { defaultMessage: 'The rules export file is not an array' } + ), + EMPTY: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.empty', + { defaultMessage: 'The rules export file is empty' } + ), + NOT_OBJECT: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notObject', + { defaultMessage: 'The rules export file contains non-object entries' } + ), + WRONG_FORMAT: (formatError: string) => { + return i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.wrongFormat', + { + defaultMessage: 'The rules export file has wrong format: {formatError}', + values: { formatError }, + } + ); + }, +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts new file mode 100644 index 0000000000000..25e9e398e7e6f --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts @@ -0,0 +1,10 @@ +/* + * 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 interface SubStepProps { + onComplete: () => void; +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 37d069ede397a..bd82768e5b519 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -11,7 +11,6 @@ import type { CreateRuleMigrationRequestBody, CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, - StartRuleMigrationRequestBody, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants'; import type { StartPluginsDependencies } from '../../../types'; From a486424e3af22c2093cc4c44466894f616075d42 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 3 Dec 2024 15:27:28 +0100 Subject: [PATCH 14/48] basic workflow working --- .../siem_migrations/model/common.gen.ts | 5 +- .../siem_migrations/model/common.schema.yaml | 2 +- .../model/rule_migration.gen.ts | 21 +- .../model/rule_migration.schema.yaml | 22 ++- .../siem_migrations/ai_connector/index.ts | 2 +- .../start_migration/context.tsx | 11 +- .../siem_migrations/start_migration/index.ts | 3 +- .../panels/migration_progress_panel.tsx | 45 +++++ .../panels/migration_ready_panel.tsx | 68 +++++++ .../panels/migration_result_panel.tsx | 87 +++++++++ .../{ => panels}/upload_rules_panel.styles.ts | 5 - .../{ => panels}/upload_rules_panel.tsx | 21 +- .../start_migration_card.styles.ts | 20 ++ .../start_migration/start_migration_card.tsx | 39 ++-- .../start_migration_check_complete.ts | 9 +- .../start_migration/translations.ts | 54 ++++++ .../siem_migrations/start_migration/types.ts | 12 -- .../start_migration/upload_rules_panels.tsx | 17 +- .../public/siem_migrations/rules/api/api.ts | 22 ++- .../components/data_input_flyout/constants.ts | 6 +- .../data_input_flyout/data_input_flyout.tsx | 55 ++++-- .../steps/rules/rules_data_input.tsx | 102 ++++++---- .../rules/sub_steps/check_resources/index.tsx | 30 +++ .../sub_steps/check_resources/translations.ts | 20 ++ .../copy_export_query.tsx | 18 +- .../sub_steps/copy_export_query/index.tsx | 26 +++ .../copy_export_query/translations.ts | 18 ++ .../rules/sub_steps/rules_file_upload.tsx | 179 ------------------ .../sub_steps/rules_file_upload/index.tsx | 58 ++++++ .../rules_file_upload/parse_rules_file.ts | 76 ++++++++ .../rules_file_upload/rules_file_upload.tsx | 122 ++++++++++++ .../rules_file_upload/translations.ts | 70 +++++++ .../steps/rules/translations.ts | 54 ------ .../components/data_input_flyout/types.ts | 6 +- .../siem_migrations/rules/pages/index.tsx | 2 +- .../hooks/common/api_request_reducer.ts | 26 +++ .../service/hooks/use_create_migration.ts | 56 ++++++ .../{ => service}/hooks/use_latest_stats.ts | 2 +- .../service/hooks/use_start_migration.ts | 52 +++++ .../rules/service/rule_migrations_service.ts | 12 +- .../rules/data/rule_migrations_field_maps.ts | 3 +- .../siem_migrations/rules/task/agent/graph.ts | 2 +- .../match_prebuilt_rule.ts | 2 +- .../rules/task/rule_migrations_task_client.ts | 2 +- 44 files changed, 1075 insertions(+), 389 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/{ => panels}/upload_rules_panel.styles.ts (75%) rename x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/{ => panels}/upload_rules_panel.tsx (80%) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/translations.ts rename x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/{ => copy_export_query}/copy_export_query.tsx (81%) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/common/api_request_reducer.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts rename x-pack/plugins/security_solution/public/siem_migrations/rules/{ => service}/hooks/use_latest_stats.ts (91%) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts index 9b1d0756c3a3b..9b2cb12be785b 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts @@ -20,10 +20,7 @@ import { z } from '@kbn/zod'; * A string that is not empty and does not contain only whitespace */ export type NonEmptyString = z.infer; -export const NonEmptyString = z - .string() - .min(1) - .regex(/^(?! *$).+$/); +export const NonEmptyString = z.string().min(1).regex(/\S+/); /** * The GenAI connector id to use. diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml index a50225df778ad..5799c092b777b 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml @@ -8,7 +8,7 @@ components: schemas: NonEmptyString: type: string - pattern: ^(?! *$).+$ + pattern: \S+ minLength: 1 description: A string that is not empty and does not contain only whitespace ConnectorId: diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 82e3c5549fd86..89525c625b2dc 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -24,6 +24,19 @@ import { NonEmptyString } from './common.gen'; export type OriginalRuleVendor = z.infer; export const OriginalRuleVendor = z.literal('splunk'); +/** + * The original rule annotations containing additional information. + */ +export type OriginalRuleAnnotations = z.infer; +export const OriginalRuleAnnotations = z + .object({ + /** + * The original rule Mitre Attack IDs. + */ + mitre_attack: z.array(z.string()).optional(), + }) + .catchall(z.unknown()); + /** * The original rule to migrate. */ @@ -40,7 +53,7 @@ export const OriginalRule = z.object({ /** * The original rule name. */ - title: z.string(), + title: NonEmptyString, /** * The original rule description. */ @@ -48,15 +61,15 @@ export const OriginalRule = z.object({ /** * The original rule query. */ - query: z.string(), + query: z.string().min(1), /** * The original rule query language. */ query_language: z.string(), /** - * The original rule Mitre Attack technique IDs. + * The original rule annotations containing additional information. */ - mitre_attack_ids: z.array(z.string()).optional(), + annotations: OriginalRuleAnnotations.optional(), }); /** diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 82892b4fa0722..d4d83cd868e2e 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -12,6 +12,17 @@ components: enum: - splunk + OriginalRuleAnnotations: + type: object + description: The original rule annotations containing additional information. + additionalProperties: true + properties: + mitre_attack: + type: array + description: The original rule Mitre Attack IDs. + items: + type: string + OriginalRule: type: object description: The original rule to migrate. @@ -30,22 +41,21 @@ components: description: The original rule vendor identifier. $ref: '#/components/schemas/OriginalRuleVendor' title: - type: string description: The original rule name. + $ref: './common.schema.yaml#/components/schemas/NonEmptyString' description: type: string description: The original rule description. query: type: string + minLength: 1 description: The original rule query. query_language: type: string description: The original rule query language. - mitre_attack_ids: - type: array - items: - type: string - description: The original rule Mitre Attack technique IDs. + annotations: + description: The original rule annotations containing additional information. + $ref: '#/components/schemas/OriginalRuleAnnotations' ElasticRule: type: object diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts index defa83eda98e6..d0b32eb1bd638 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts @@ -6,7 +6,7 @@ */ import React from 'react'; -import { AssistantAvatar } from '@kbn/elastic-assistant'; +import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; import type { OnboardingCardConfig } from '../../../../../types'; import { OnboardingCardId } from '../../../../../constants'; import { AI_CONNECTOR_CARD_TITLE } from './translations'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx index bc964da73f645..49baaba65caca 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx @@ -6,17 +6,22 @@ */ import React, { createContext, useContext, useMemo, type PropsWithChildren } from 'react'; +import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; interface StartMigrationContextValue { - setIsFlyoutOpen: React.Dispatch>; + openFlyout: (migrationStats?: RuleMigrationTaskStats) => void; + closeFlyout: () => void; } const StartMigrationContext = createContext(null); export const StartMigrationContextProvider: React.FC< PropsWithChildren -> = React.memo(({ children, setIsFlyoutOpen }) => { - const value = useMemo(() => ({ setIsFlyoutOpen }), [setIsFlyoutOpen]); +> = React.memo(({ children, openFlyout, closeFlyout }) => { + const value = useMemo( + () => ({ openFlyout, closeFlyout }), + [openFlyout, closeFlyout] + ); return {children}; }); StartMigrationContextProvider.displayName = 'StartMigrationContextProvider'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts index 74cd180c11ef9..e2b8f5f7da0db 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts @@ -11,9 +11,8 @@ import { OnboardingCardId } from '../../../../../constants'; import { START_MIGRATION_CARD_TITLE } from './translations'; import cardIcon from './images/card_header_icon.png'; import { checkStartMigrationCardComplete } from './start_migration_check_complete'; -import type { StartMigrationCardMetadata } from './types'; -export const startMigrationCardConfig: OnboardingCardConfig = { +export const startMigrationCardConfig: OnboardingCardConfig = { id: OnboardingCardId.siemMigrationsStart, title: START_MIGRATION_CARD_TITLE, icon: cardIcon, diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx new file mode 100644 index 0000000000000..0527e1cfbdf17 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx @@ -0,0 +1,45 @@ +/* + * 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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx new file mode 100644 index 0000000000000..8603511fa2d6f --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx @@ -0,0 +1,68 @@ +/* + * 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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx new file mode 100644 index 0000000000000..4b8fca56ac5b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx @@ -0,0 +1,87 @@ +/* + * 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({ + createdAt: moment(migrationStats.created_at).format('MMMM Do YYYY, h:mm:ss a'), + finishedAt: 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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts similarity index 75% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts index d8444b324fa9d..0aef40dfeb442 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts @@ -6,10 +6,8 @@ */ import { css } from '@emotion/css'; -import { useEuiTheme } from '@elastic/eui'; export const useStyles = (compressed: boolean) => { - const { euiTheme } = useEuiTheme(); const logoSize = compressed ? '32px' : '88px'; return css` .siemMigrationsIcon { @@ -17,8 +15,5 @@ export const useStyles = (compressed: boolean) => { block-size: ${logoSize}; inline-size: ${logoSize}; } - .siemMigrationsUploadTitle { - font-weight: ${euiTheme.font.weight.semiBold}; - } `; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx index 4a408b9c94ab1..edcff3646c5aa 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx @@ -15,18 +15,21 @@ import { EuiButtonEmpty, EuiPanel, } from '@elastic/eui'; -import { SiemMigrationsIcon } from '../../../../../../siem_migrations/common/icon'; +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 { useStyles } from './upload_rules_panel.styles'; -import * as i18n from './translations'; -import { useStartMigrationContext } from './context'; export interface UploadRulesPanelProps { isUploadMore?: boolean; } export const UploadRulesPanel = React.memo(({ isUploadMore = false }) => { const styles = useStyles(isUploadMore); - const { setIsFlyoutOpen } = useStartMigrationContext(); - const openFlyout = useCallback(() => setIsFlyoutOpen(true), [setIsFlyoutOpen]); + const { openFlyout } = useStartMigrationContext(); + const onOpenFlyout = useCallback(() => { + openFlyout(); + }, [openFlyout]); return ( @@ -36,13 +39,13 @@ export const UploadRulesPanel = React.memo(({ isUploadMor {isUploadMore ? ( - +

{i18n.START_MIGRATION_CARD_UPLOAD_MORE_TITLE}

) : ( - +

{i18n.START_MIGRATION_CARD_UPLOAD_TITLE}

@@ -61,11 +64,11 @@ export const UploadRulesPanel = React.memo(({ isUploadMor
{isUploadMore ? ( - + {i18n.START_MIGRATION_CARD_UPLOAD_MORE_BUTTON} ) : ( - + {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} )} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts new file mode 100644 index 0000000000000..82446ba308402 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { css } from '@emotion/css'; +import { useEuiTheme } from '@elastic/eui'; + +export const TITLE_CLASS_NAME = 'siemMigrationsStartTitle'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + return css` + .${TITLE_CLASS_NAME} { + font-weight: ${euiTheme.font.weight.semiBold}; + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index d8c2ae45fb259..3e37cd51b12c4 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -6,34 +6,47 @@ */ import React, { useCallback, useState } from 'react'; +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 type { StartMigrationCardMetadata } from './types'; import { UploadRulesPanels } from './upload_rules_panels'; import { StartMigrationContextProvider } from './context'; +import { useStyles } from './start_migration_card.styles'; -export const StartMigrationCard: OnboardingCardComponent = ({ - checkCompleteMetadata, - checkComplete, - setComplete, -}) => { - const migrationsStats = checkCompleteMetadata?.migrationsStats; - const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); +export const StartMigrationCard: OnboardingCardComponent = ({ checkComplete, setComplete }) => { + const styles = useStyles(); + const { data: migrationsStats, isLoading } = useLatestStats(); - const onClose = useCallback(() => setIsFlyoutOpen(false), [setIsFlyoutOpen]); + const [isFlyoutOpen, setIsFlyoutOpen] = useState(); + const [flyoutMigrationStats, setFlyoutMigrationStats] = useState< + RuleMigrationTaskStats | undefined + >(); + + const closeFlyout = useCallback(() => { + setIsFlyoutOpen(false); + setFlyoutMigrationStats(undefined); + }, []); + + const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => { + setFlyoutMigrationStats(migrationStats); + setIsFlyoutOpen(true); + }, []); return ( - - - {migrationsStats == null ? ( + + + {isLoading ? ( ) : ( )} - {isFlyoutOpen && } + {isFlyoutOpen && ( + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts index f0a14d56b1b27..9c98ef0f27da5 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts @@ -6,12 +6,11 @@ */ import type { OnboardingCardCheckComplete } from '../../../../../types'; -import type { StartMigrationCardMetadata } from './types'; -export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< - StartMigrationCardMetadata -> = async ({ siemMigrations }) => { +export const checkStartMigrationCardComplete: OnboardingCardCheckComplete = async ({ + siemMigrations, +}) => { const migrationsStats = await siemMigrations.rules.getRuleMigrationTasksStats(); const isComplete = migrationsStats.length > 0; - return { isComplete, metadata: { migrationsStats } }; + return isComplete; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index 7c3090d5f4da6..ebd495eb2506b 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -48,3 +48,57 @@ 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 = (values: { + createdAt: string; + finishedAt: string; +}) => + i18n.translate('xpack.securitySolution.onboarding.startMigration.result.description', { + defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.', + values, + }); + +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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts deleted file mode 100644 index 68a8f5edf76d6..0000000000000 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts +++ /dev/null @@ -1,12 +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 type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types'; - -export interface StartMigrationCardMetadata { - migrationsStats: RuleMigrationStats[]; -} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx index 4107ddff25dfd..95b53d921fd1f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx @@ -9,7 +9,10 @@ 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 { 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[]; @@ -20,17 +23,20 @@ export const UploadRulesPanels = React.memo(({ migration } return ( - + {migrationsStats.map((migrationStats) => ( + {migrationStats.status === SiemMigrationTaskStatus.READY && ( + + )} {migrationStats.status === SiemMigrationTaskStatus.RUNNING && ( - + )} {migrationStats.status === SiemMigrationTaskStatus.FINISHED && ( - + )} ))} @@ -38,6 +44,3 @@ export const UploadRulesPanels = React.memo(({ migration ); }); UploadRulesPanels.displayName = 'UploadRulesPanels'; - -const RuleMigrationProcess = () => null; -const RuleMigrationResult = () => null; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts index 1debf6f43f57d..92ff559a39cc0 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -11,11 +11,13 @@ import type { LangSmithOptions } from '../../../../common/siem_migrations/model/ import { KibanaServices } from '../../../common/lib/kibana'; import { + SIEM_RULE_MIGRATIONS_PATH, SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH, SIEM_RULE_MIGRATION_INSTALL_PATH, SIEM_RULE_MIGRATION_PATH, SIEM_RULE_MIGRATION_START_PATH, + SIEM_RULE_MIGRATION_STATS_PATH, } from '../../../../common/siem_migrations/constants'; import type { CreateRuleMigrationRequestBody, @@ -25,8 +27,26 @@ import type { InstallTranslatedMigrationRulesResponse, InstallMigrationRulesResponse, StartRuleMigrationRequestBody, + GetRuleMigrationStatsResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +export interface GetRuleMigrationsStatsParams { + /** `id` of the migration to get stats for */ + migrationId: string; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Retrieves the stats for all the existing migrations, aggregated by `migration_id`. */ +export const getRuleMigrationsStats = async ({ + migrationId, + signal, +}: GetRuleMigrationsStatsParams): Promise => { + return KibanaServices.get().http.fetch( + replaceParams(SIEM_RULE_MIGRATION_STATS_PATH, { migration_id: migrationId }), + { method: 'GET', version: '1', signal } + ); +}; + export interface GetRuleMigrationsStatsAllParams { /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; @@ -52,7 +72,7 @@ export const createRuleMigration = async ({ body, signal, }: CreateRuleMigrationParams): Promise => { - return KibanaServices.get().http.post(SIEM_RULE_MIGRATION_PATH, { + return KibanaServices.get().http.post(SIEM_RULE_MIGRATIONS_PATH, { body: JSON.stringify(body), version: '1', signal, diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts index c874672399aaf..bc118ee3b1518 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts @@ -20,7 +20,7 @@ export const SPL_RULES_COLUMNS = [ 'action.correlationsearch.annotations', ] as const; -export const RULES_SPL_QUERY = `| rest /servicesNS/-/Splunk_Security_Essentials/saved/searches -| union [ rest /servicesNS/-/SplunkEnterpriseSecuritySuite/saved/searches ] -| search is_scheduled=1 disabled=0 +export const RULES_SPL_QUERY = `| rest splunk_server=local /servicesNS/-/-/saved/searches +| search action.correlationsearch.enabled = "1" OR (eai:acl.app = "Splunk_Security_Essentials" AND is_scheduled=1) +| where disabled=0 | table ${SPL_RULES_COLUMNS.join(', ')}`; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index 15f524a56843c..6a4916a5e54b3 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -17,38 +17,45 @@ import { EuiButton, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { DataInputStep } from './constants'; import { RulesDataInput } from './steps/rules/rules_data_input'; +import { useStartMigration } from '../../service/hooks/use_start_migration'; export interface MigrationDataInputFlyoutProps { onClose: () => void; - migrationId?: string; - dataInputStep?: DataInputStep; + migrationStats?: RuleMigrationTaskStats; } export const MigrationDataInputFlyout = React.memo( - ({ - onClose, - migrationId: initialMigrationId, - dataInputStep: initialDataInputStep = DataInputStep.rules, - }) => { - const { siemMigrations } = useKibana().services; - const [migrationId, setMigrationId] = useState(initialMigrationId); + ({ onClose, migrationStats: initialMigrationSats }) => { + const [migrationStats, setMigrationStats] = useState( + initialMigrationSats + ); - const onStart = useCallback(() => { - if (migrationId) { - siemMigrations.rules.startRuleMigration(migrationId); - onClose(); + const { startMigration, isLoading: isStartLoading } = useStartMigration(onClose); + const onStartMigration = useCallback(() => { + if (migrationStats?.id) { + startMigration(migrationStats.id); } - }, [migrationId, siemMigrations.rules, onClose]); + }, [migrationStats, startMigration]); const [dataInputStep, setDataInputStep] = useState(() => { - if (initialMigrationId && initialDataInputStep === DataInputStep.rules) { - return DataInputStep.macros; // if initialMigrationId is defined the rules step is not available anymore. + if (migrationStats) { + return DataInputStep.macros; } - return initialDataInputStep; + return DataInputStep.rules; }); + const onMigrationCreated = useCallback( + (createdMigrationStats: RuleMigrationTaskStats) => { + if (createdMigrationStats) { + setMigrationStats(createdMigrationStats); + setDataInputStep(DataInputStep.macros); + } + }, + [setDataInputStep] + ); + return ( - + @@ -81,7 +91,12 @@ export const MigrationDataInputFlyout = React.memo - + { if (step === currentStep) { return 'current'; @@ -31,47 +33,63 @@ const getStatus = (step: Step, currentStep: Step): EuiStepStatus => { return 'incomplete'; }; -export const RulesDataInput = React.memo(() => { - const [step, setStep] = useState(1); +interface RulesDataInputProps { + selected: boolean; + onMigrationCreated: OnMigrationCreated; +} - const steps = useMemo( - () => [ - { - title: 'Copy rule query', - status: getStatus(1, step), - children: setStep(2)} />, +export const RulesDataInput = React.memo( + ({ selected, onMigrationCreated }) => { + const [step, setStep] = useState(1); + + const copyStep = useCopyExportQueryStep({ + status: getStatus(1, step), + onCopied: () => setStep(2), + }); + + const uploadStep = useRulesFileUploadStep({ + status: getStatus(2, step), + onMigrationCreated: (stats) => { + onMigrationCreated(stats); + setStep(3); }, - { - title: 'Update your rule export', - status: getStatus(2, step), - children: setStep(3)} />, + }); + + const resourcesStep = useCheckResourcesStep({ + status: getStatus(3, step), + onComplete: () => { + setStep(4); }, - ], - [setStep, step] - ); + }); - return ( - - - - - - - - - - {i18n.RULES_DATA_INPUT_TITLE} - - - - - - - - - - - - ); -}); + const steps = useMemo( + () => [copyStep, uploadStep, resourcesStep], + [copyStep, uploadStep, resourcesStep] + ); + + return ( + + + + + + + + + + {i18n.RULES_DATA_INPUT_TITLE} + + + + + + + + + + + + ); + } +); RulesDataInput.displayName = 'RulesDataInput'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx new file mode 100644 index 0000000000000..3b081eb203267 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx @@ -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 React from 'react'; +import { EuiText, type EuiStepProps, type EuiStepStatus } from '@elastic/eui'; +import * as i18n from './translations'; + +export interface CheckResourcesStepProps { + status: EuiStepStatus; + onComplete: () => void; +} +export const useCheckResourcesStep = ({ + status, + onComplete, +}: CheckResourcesStepProps): EuiStepProps => { + // onComplete(); // TODO: check the resources + return { + title: i18n.RULES_DATA_INPUT_CHECK_RESOURCES_TITLE, + status, + children: ( + + {i18n.RULES_DATA_INPUT_CHECK_RESOURCES_DESCRIPTION} + + ), + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/translations.ts new file mode 100644 index 0000000000000..159b4033fafd6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/translations.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULES_DATA_INPUT_CHECK_RESOURCES_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.checkResources.title', + { defaultMessage: 'Check for macros and lookups' } +); + +export const RULES_DATA_INPUT_CHECK_RESOURCES_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.checkResources.description', + { + defaultMessage: `For best translation results, we will automatically review your rules for macros and lookups and ask you to upload them. Once uploaded, we'll be able to deliver a more complete rule translation for all rules using those macros or lookups.`, + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query.tsx rename to x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx index 0b01985e28b3d..863dd3864c925 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx @@ -8,19 +8,21 @@ import React, { useCallback } from 'react'; import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { SubStepProps } from '../../../types'; -import { RULES_SPL_QUERY } from '../../../constants'; -import * as i18n from '../translations'; +import { RULES_SPL_QUERY } from '../../../../constants'; +import * as i18n from './translations'; -export const CopyExportQuery = React.memo(({ onComplete }) => { +interface CopyExportQueryProps { + onCopied: () => void; +} +export const CopyExportQuery = React.memo(({ onCopied }) => { const onClick: React.MouseEventHandler = useCallback( (ev) => { // The only button inside the element is the "copy" button. if ((ev.target as Element).tagName === 'BUTTON') { - onComplete(); + onCopied(); } }, - [onComplete] + [onCopied] ); return ( @@ -30,7 +32,7 @@ export const CopyExportQuery = React.memo(({ onComplete }) => { {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
{/* onCopy react event is dispatched when the user copies text manually */} - + {RULES_SPL_QUERY}
@@ -40,7 +42,7 @@ export const CopyExportQuery = React.memo(({ onComplete }) => { 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_DESCRIPTION_SECTION}, + section: {i18n.RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION}, format: {'JSON'}, }} /> diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/index.tsx new file mode 100644 index 0000000000000..3d2adcc78857b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { CopyExportQuery } from './copy_export_query'; +import * as i18n from './translations'; + +export interface CopyExportQueryStepProps { + status: EuiStepStatus; + onCopied: () => void; +} +export const useCopyExportQueryStep = ({ + status, + onCopied, +}: CopyExportQueryStepProps): EuiStepProps => { + return { + title: i18n.RULES_DATA_INPUT_COPY_TITLE, + status, + children: , + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts new file mode 100644 index 0000000000000..d76eb71f2e378 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts @@ -0,0 +1,18 @@ +/* + * 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 RULES_DATA_INPUT_COPY_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.title', + { defaultMessage: 'Copy and export query' } +); + +export const RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.description.section', + { defaultMessage: 'Search and Reporting' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload.tsx deleted file mode 100644 index 6e5e74196aeae..0000000000000 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload.tsx +++ /dev/null @@ -1,179 +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, useState } from 'react'; -import { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; -import { isPlainObject } from 'lodash/fp'; -import type { OriginalRuleAnnotations } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; -import { OriginalRule } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; -import * as i18n from '../translations'; -import type { SubStepProps } from '../../../types'; -import type { SPL_RULES_COLUMNS } from '../../../constants'; - -type ExpectedRowFormat = Partial>; - -const parseContent = (fileContent: string): OriginalRule[] => { - const trimmedContent = fileContent.trim(); - let arrayContent: ExpectedRowFormat[]; - if (trimmedContent.startsWith('[')) { - arrayContent = parseJSONArray(trimmedContent); - } else { - arrayContent = parseNDJSON(trimmedContent); - } - if (arrayContent.length === 0) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.EMPTY); - } - return arrayContent.map(convertFormat); -}; - -const parseNDJSON = (fileContent: string): ExpectedRowFormat[] => { - return fileContent - .split(/\n(?=\{)/) // split at newline followed by '{'. - .filter((entry) => entry.trim() !== '') // Remove empty entries. - .map((entry) => JSON.parse(entry)); // Parse each entry as JSON. -}; - -const parseJSONArray = (fileContent: string): ExpectedRowFormat[] => { - let parsedContent: ExpectedRowFormat; - try { - parsedContent = JSON.parse(fileContent); - } catch (error) { - if (error instanceof RangeError) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); - } - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_PARSE); - } - if (!Array.isArray(parsedContent)) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_ARRAY); - } - return parsedContent; -}; - -const convertFormat = (row: ExpectedRowFormat): OriginalRule => { - if (!isPlainObject(row)) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_OBJECT); - } - const originalRule: Partial = { - id: row.id, - vendor: 'splunk', - title: row.title, - query: row.search, - query_language: 'spl', - description: row['action.escu.eli5'] || row.description, - }; - - if (row['action.correlationsearch.annotations']) { - originalRule.annotations = processAnnotations(row['action.correlationsearch.annotations']); - } - // Validates each rule format. - const { error } = OriginalRule.safeParse(originalRule); - if (error) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.WRONG_FORMAT(error.message)); - } - return originalRule as OriginalRule; -}; - -const processAnnotations = (annotationsStr: string): OriginalRuleAnnotations | undefined => { - try { - return JSON.parse(annotationsStr); - } catch (error) { - return undefined; - } -}; - -export const RulesFileUpload = React.memo(({ onComplete }) => { - const [isParsing, setIsParsing] = useState(false); - const [fileError, setFileError] = useState(); - - const onChangeFile = useCallback((files: FileList | null) => { - if (!files) { - return; - } - - setFileError(undefined); - - const logsSampleFile = files[0]; - const reader = new FileReader(); - - reader.onloadstart = () => setIsParsing(true); - reader.onloadend = () => setIsParsing(false); - - reader.onload = function (e) { - const fileContent = e.target?.result as string | undefined; // We can safely cast to string since we call `readAsText` to load the file. - - if (fileContent == null) { - setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); - return; - } - - if (fileContent === '' && 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 - setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); - return; - } - - let data: object[]; - try { - data = parseContent(fileContent); - } catch (error) { - setFileError(error); - } - - // TODO: Create a new migration using data - - // setIntegrationSettings({ - // ...integrationSettings, - // logSamples, - // samplesFormat, - // }); - }; - - const handleReaderError = function () { - const message = reader.error?.message; - if (message) { - setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); - } else { - setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); - } - }; - - reader.onerror = handleReaderError; - reader.onabort = handleReaderError; - - reader.readAsText(logsSampleFile); - }, []); - - return ( - - {fileError} - - } - isInvalid={fileError != null} - > - - - {i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT} - - - } - onChange={onChangeFile} - display="large" - aria-label="Upload logs sample file" - isLoading={isParsing} - data-test-subj="logsSampleFilePicker" - data-loading={isParsing} - /> - - ); -}); -RulesFileUpload.displayName = 'RulesFileUpload'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx new file mode 100644 index 0000000000000..ab7838b28908b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx @@ -0,0 +1,58 @@ +/* + * 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 type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import type { OnMigrationCreated } from '../../../../types'; +import { RulesFileUpload } from './rules_file_upload'; +import { + useCreateMigration, + type OnSuccess, +} from '../../../../../../service/hooks/use_create_migration'; +import * as i18n from './translations'; + +export interface RulesFileUploadStepProps { + status: EuiStepStatus; + onMigrationCreated: OnMigrationCreated; +} +export const useRulesFileUploadStep = ({ + status, + onMigrationCreated, +}: RulesFileUploadStepProps): EuiStepProps => { + const [isCreated, setIsCreated] = useState(false); + const onSuccess = useCallback( + (stats) => { + setIsCreated(true); + onMigrationCreated(stats); + }, + [onMigrationCreated] + ); + const { createMigration, isLoading, error } = useCreateMigration(onSuccess); + + const uploadStepStatus = useMemo(() => { + if (isLoading) { + return 'loading'; + } + if (error) { + return 'danger'; + } + return status; + }, [isLoading, error, status]); + + return { + title: i18n.RULES_DATA_INPUT_FILE_UPLOAD_TITLE, + status: uploadStepStatus, + children: ( + + ), + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts new file mode 100644 index 0000000000000..be90d3b9e3bb1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts @@ -0,0 +1,76 @@ +/* + * 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 { isPlainObject } from 'lodash/fp'; +import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { SPL_RULES_COLUMNS } from '../../../../constants'; +import * as i18n from './translations'; + +type SplunkResult = Partial>; +interface SplunkRow { + result: SplunkResult; +} + +export const parseContent = (fileContent: string): OriginalRule[] => { + const trimmedContent = fileContent.trim(); + let arrayContent: SplunkRow[]; + if (trimmedContent.startsWith('[')) { + arrayContent = parseJSONArray(trimmedContent); + } else { + arrayContent = parseNDJSON(trimmedContent); + } + if (arrayContent.length === 0) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.EMPTY); + } + return arrayContent.map(convertFormat); +}; + +const parseNDJSON = (fileContent: string): SplunkRow[] => { + return fileContent + .split(/\n(?=\{)/) // split at newline followed by '{'. + .filter((entry) => entry.trim() !== '') // Remove empty entries. + .map((entry) => JSON.parse(entry)); // Parse each entry as JSON. +}; + +const parseJSONArray = (fileContent: string): SplunkRow[] => { + let parsedContent: SplunkResult; + try { + parsedContent = JSON.parse(fileContent); + } catch (error) { + if (error instanceof RangeError) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + } + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_PARSE); + } + if (!Array.isArray(parsedContent)) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_ARRAY); + } + return parsedContent; +}; + +const convertFormat = (row: SplunkRow): OriginalRule => { + if (!isPlainObject(row) || !isPlainObject(row.result)) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_OBJECT); + } + const originalRule: Partial = { + id: row.result.id, + vendor: 'splunk', + title: row.result.title, + query: row.result.search, + query_language: 'spl', + description: row.result['action.escu.eli5'] || row.result.description, + }; + + if (row.result['action.correlationsearch.annotations']) { + try { + originalRule.annotations = JSON.parse(row.result['action.correlationsearch.annotations']); + } catch (error) { + delete originalRule.annotations; + } + } + return originalRule as OriginalRule; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx new file mode 100644 index 0000000000000..0f9787a4ddf68 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx @@ -0,0 +1,122 @@ +/* + * 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 { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; +import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { CreateMigration } from '../../../../../../service/hooks/use_create_migration'; +import { parseContent } from './parse_rules_file'; +import * as i18n from './translations'; + +export interface RulesFileUploadProps { + createMigration: CreateMigration; + apiError?: string; + isLoading?: boolean; + isCreated?: boolean; +} +export const RulesFileUpload = React.memo( + ({ createMigration, apiError, isLoading, isCreated }) => { + const [isParsing, setIsParsing] = useState(false); + const [fileError, setFileError] = useState(); + + const onChangeFile = useCallback( + (files: FileList | null) => { + if (!files) { + return; + } + + setFileError(undefined); + + const rulesFile = files[0]; + 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 fileContent = e.target?.result as string | undefined; + + if (fileContent == null) { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); + return; + } + + if (fileContent === '' && 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 + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + return; + } + + let data: OriginalRule[]; + try { + data = parseContent(fileContent); + createMigration(data); + } catch (err) { + setFileError(err.message); + } + }; + + const handleReaderError = function () { + const message = reader.error?.message; + if (message) { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); + } else { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); + } + }; + + reader.onerror = handleReaderError; + reader.onabort = handleReaderError; + + reader.readAsText(rulesFile); + }, + [createMigration] + ); + + const error = useMemo(() => { + if (apiError) { + return apiError; + } + return fileError; + }, [apiError, fileError]); + + return ( + + {error} + + } + isInvalid={error != null} + fullWidth + > + + + {i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT} + + + } + accept="application/json" + onChange={onChangeFile} + display="large" + aria-label="Upload logs sample file" + isLoading={isParsing || isLoading} + disabled={isLoading || isCreated} + data-test-subj="rulesFilePicker" + data-loading={isParsing} + /> + + ); + } +); +RulesFileUpload.displayName = 'RulesFileUpload'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts new file mode 100644 index 0000000000000..675eed61f4973 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULES_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.title', + { defaultMessage: 'Update your rule export' } +); +export const RULES_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.prompt', + { defaultMessage: 'Select or drag and drop the exported JSON file' } +); + +export const RULES_DATA_INPUT_FILE_UPLOAD_ERROR = { + CAN_NOT_READ: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotRead', + { defaultMessage: 'Failed to read the rules export file' } + ), + CAN_NOT_READ_WITH_REASON: (reason: string) => + i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotReadWithReason', + { + defaultMessage: 'An error occurred when reading rules export file: {reason}', + values: { reason }, + } + ), + CAN_NOT_PARSE: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotParse', + { defaultMessage: 'Cannot parse the rules export file as either a JSON file' } + ), + TOO_LARGE_TO_PARSE: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.tooLargeToParse', + { defaultMessage: 'This rules export file is too large to parse' } + ), + NOT_ARRAY: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notArray', + { defaultMessage: 'The rules export file is not an array' } + ), + EMPTY: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.empty', + { defaultMessage: 'The rules export file is empty' } + ), + NOT_OBJECT: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notObject', + { defaultMessage: 'The rules export file contains non-object entries' } + ), + WRONG_FORMAT: (formatError: string) => { + return i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.wrongFormat', + { + defaultMessage: 'The rules export file has wrong format: {formatError}', + values: { formatError }, + } + ); + }, +}; + +export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createSuccess', + { defaultMessage: 'Rules uploaded successfully' } +); +export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createError', + { defaultMessage: 'Failed to upload rules file' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts index 014e97f81ce05..5446180d03a75 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts @@ -11,57 +11,3 @@ export const RULES_DATA_INPUT_TITLE = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.title', { defaultMessage: 'Upload rule export and check for macros and lookups' } ); - -export const RULES_DATA_INPUT_DESCRIPTION_SECTION = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.description.section', - { defaultMessage: 'Search and Reporting' } -); - -export const RULES_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.prompt', - { defaultMessage: 'Select or drag and drop the exported JSON file' } -); - -export const RULES_DATA_INPUT_FILE_UPLOAD_ERROR = { - CAN_NOT_READ: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotRead', - { defaultMessage: 'Failed to read the rules export file' } - ), - CAN_NOT_READ_WITH_REASON: (reason: string) => - i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotReadWithReason', - { - defaultMessage: 'An error occurred when reading rules export file: {reason}', - values: { reason }, - } - ), - CAN_NOT_PARSE: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotParse', - { defaultMessage: 'Cannot parse the rules export file as either a JSON file' } - ), - TOO_LARGE_TO_PARSE: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.tooLargeToParse', - { defaultMessage: 'This rules export file is too large to parse' } - ), - NOT_ARRAY: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notArray', - { defaultMessage: 'The rules export file is not an array' } - ), - EMPTY: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.empty', - { defaultMessage: 'The rules export file is empty' } - ), - NOT_OBJECT: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notObject', - { defaultMessage: 'The rules export file contains non-object entries' } - ), - WRONG_FORMAT: (formatError: string) => { - return i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.wrongFormat', - { - defaultMessage: 'The rules export file has wrong format: {formatError}', - values: { formatError }, - } - ); - }, -}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts index 25e9e398e7e6f..16d8f60043bcb 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts @@ -5,6 +5,6 @@ * 2.0. */ -export interface SubStepProps { - onComplete: () => void; -} +import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; + +export type OnMigrationCreated = (migrationStats: RuleMigrationTaskStats) => void; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx index dabdb83cccbab..1b556b355e8c4 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -20,7 +20,7 @@ import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/c import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout'; import { HeaderButtons } from '../components/header_buttons'; import { UnknownMigration } from '../components/unknown_migration'; -import { useLatestStats } from '../hooks/use_latest_stats'; +import { useLatestStats } from '../service/hooks/use_latest_stats'; type RulesMigrationPageProps = RouteComponentProps<{ migrationId?: string }>; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/common/api_request_reducer.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/common/api_request_reducer.ts new file mode 100644 index 0000000000000..a68432d48bf9c --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/common/api_request_reducer.ts @@ -0,0 +1,26 @@ +/* + * 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 interface State { + loading: boolean; + error: Error | null; +} +export type Action = { type: 'start' } | { type: 'error'; error: Error } | { type: 'success' }; + +export const initialState: State = { loading: false, error: null }; +export const reducer = (state: State, action: Action) => { + switch (action.type) { + case 'start': + return { loading: true, error: null }; + case 'error': + return { loading: false, error: action.error }; + case 'success': + return { loading: false, error: null }; + default: + return state; + } +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts new file mode 100644 index 0000000000000..6a4ed55fa9c7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts @@ -0,0 +1,56 @@ +/* + * 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 { useCallback, useReducer } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { CreateRuleMigrationRequestBody } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { reducer, initialState } from './common/api_request_reducer'; + +export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess', + { defaultMessage: 'Rules uploaded successfully' } +); +export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.createRuleError', + { defaultMessage: 'Failed to upload rules file' } +); + +export type CreateMigration = (data: CreateRuleMigrationRequestBody) => void; +export type OnSuccess = (migrationStats: RuleMigrationTaskStats) => void; + +export const useCreateMigration = (onSuccess: OnSuccess) => { + const { siemMigrations, notifications } = useKibana().services; + const [state, dispatch] = useReducer(reducer, initialState); + + const createMigration = useCallback( + (data) => { + (async () => { + try { + dispatch({ type: 'start' }); + const res = await siemMigrations.rules.createRuleMigration(data); + const { migration_id: migrationId } = res; + const stats = await siemMigrations.rules.getRuleMigrationsStats(migrationId); + + notifications.toasts.addSuccess(RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS); + onSuccess(stats); + dispatch({ type: 'success' }); + } catch (err) { + const apiError = err.body ?? err; + notifications.toasts.addError(apiError, { + title: RULES_DATA_INPUT_CREATE_MIGRATION_ERROR, + }); + dispatch({ type: 'error', error: apiError }); + } + })(); + }, + [siemMigrations.rules, notifications.toasts, onSuccess] + ); + + return { isLoading: state.loading, error: state.error, createMigration }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts similarity index 91% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts rename to x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts index c681af0d2a21c..8b692f07eb3cb 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts @@ -7,7 +7,7 @@ import useObservable from 'react-use/lib/useObservable'; import { useEffect, useMemo } from 'react'; -import { useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; export const useLatestStats = () => { const { siemMigrations } = useKibana().services; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts new file mode 100644 index 0000000000000..f7f76809b06a2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useReducer } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { reducer, initialState } from './common/api_request_reducer'; + +export const RULES_DATA_INPUT_START_MIGRATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.startMigrationSuccess', + { defaultMessage: 'Migration started successfully.' } +); +export const RULES_DATA_INPUT_START_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.startMigrationError', + { defaultMessage: 'Error starting migration.' } +); + +export type StartMigration = (migrationId: string) => void; +export type OnSuccess = () => void; + +export const useStartMigration = (onSuccess?: OnSuccess) => { + const { siemMigrations, notifications } = useKibana().services; + const [state, dispatch] = useReducer(reducer, initialState); + + const startMigration = useCallback( + (migrationId) => { + dispatch({ type: 'start' }); + (async () => { + try { + await siemMigrations.rules.startRuleMigration(migrationId); + + notifications.toasts.addSuccess(RULES_DATA_INPUT_START_MIGRATION_SUCCESS); + dispatch({ type: 'success' }); + onSuccess?.(); + } catch (err) { + const apiError = err.body ?? err; + notifications.toasts.addError(apiError, { + title: RULES_DATA_INPUT_START_MIGRATION_ERROR, + }); + dispatch({ type: 'error', error: apiError }); + } + })(); + }, + [siemMigrations.rules, notifications.toasts, onSuccess] + ); + + return { isLoading: state.loading, error: state.error, startMigration }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index bd82768e5b519..4df57623429db 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -11,13 +11,19 @@ import type { CreateRuleMigrationRequestBody, CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, + GetRuleMigrationStatsResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants'; import type { StartPluginsDependencies } from '../../../types'; import { ExperimentalFeaturesService } from '../../../common/experimental_features_service'; import { licenseService } from '../../../common/hooks/use_license'; import type { GetRuleMigrationsStatsAllParams } from '../api/api'; -import { createRuleMigration, getRuleMigrationsStatsAll, startRuleMigration } from '../api/api'; +import { + createRuleMigration, + getRuleMigrationsStats, + getRuleMigrationsStatsAll, + startRuleMigration, +} from '../api/api'; import type { RuleMigrationStats } from '../types'; import { getSuccessToast } from './success_notification'; import { RuleMigrationsStorage } from './storage'; @@ -89,6 +95,10 @@ export class SiemRulesMigrationsService { return createRuleMigration({ body }); } + public async getRuleMigrationsStats(migrationId: string): Promise { + return getRuleMigrationsStats({ migrationId }); + } + public async startRuleMigration(migrationId: string): Promise { const connectorId = this.connectorIdStorage.get(); if (!connectorId) { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index 8e8a3c5ee0f27..fe767987570f7 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -23,7 +23,8 @@ export const ruleMigrationsFieldMap: FieldMap { if (state.elastic_rule?.prebuilt_rule_id) { return END; } - return 'translation'; + return 'translationSubGraph'; }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts index 8f0460165f290..5900f45912599 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts @@ -24,7 +24,7 @@ interface GetMatchedRuleResponse { export const getMatchPrebuiltRuleNode = ({ model, prebuiltRulesMap }: GetMatchPrebuiltRuleNodeParams): GraphNode => async (state) => { - const mitreAttackIds = state.original_rule.mitre_attack_ids; + const mitreAttackIds = state.original_rule.annotations?.mitre_attack; if (!mitreAttackIds?.length) { return {}; } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index a6ea5c9040e16..c8ce10dac05b9 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -30,7 +30,7 @@ import { IntegrationRetriever } from './util/integration_retriever'; import { retrievePrebuiltRulesMap } from './util/prebuilt_rules'; import { RuleResourceRetriever } from './util/rule_resource_retriever'; -const ITERATION_BATCH_SIZE = 50 as const; +const ITERATION_BATCH_SIZE = 1 as const; const ITERATION_SLEEP_SECONDS = 10 as const; type MigrationsRunning = Map; From b02a569cbc03aaf060a38151a773b87145737b9c Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 3 Dec 2024 19:05:27 +0100 Subject: [PATCH 15/48] no connector selected callout --- .../missing_ai_connector_callout.tsx | 38 ++++++++ .../start_migration/start_migration_card.tsx | 90 ++++++++++++------- .../start_migration/translations.ts | 19 ++++ .../components/data_input_flyout/constants.ts | 2 +- .../copy_export_query/copy_export_query.tsx | 4 +- .../rules_file_upload/parse_rules_file.ts | 2 +- .../rules/service/hooks/translations.ts | 17 ++++ 7 files changed, 135 insertions(+), 37 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/translations.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx new file mode 100644 index 0000000000000..324dd405d5141 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; +import { OnboardingCardContentPanel } from '../../common/card_content_panel'; +import { CardCallOut } from '../../common/card_callout'; +import * as i18n from './translations'; + +interface MissingAIConnectorCalloutProps { + onExpandAiConnectorsCard: () => void; +} + +export const MissingAIConnectorCallout = React.memo( + ({ onExpandAiConnectorsCard }) => ( + + + + {i18n.START_MIGRATION_CARD_CONNECTOR_MISSING_BUTTON} + + + + + + } + /> + + ) +); +MissingAIConnectorCallout.displayName = 'MissingAIConnectorCallout'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index 3e37cd51b12c4..c1e7539c8e101 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -6,6 +6,8 @@ */ import React, { useCallback, useState } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +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'; @@ -15,41 +17,63 @@ import { OnboardingCardContentPanel } from '../../common/card_content_panel'; import { UploadRulesPanels } from './upload_rules_panels'; import { StartMigrationContextProvider } from './context'; import { useStyles } from './start_migration_card.styles'; +import * as i18n from './translations'; +import { MissingAIConnectorCallout } from './missing_ai_connector_callout'; -export const StartMigrationCard: OnboardingCardComponent = ({ checkComplete, setComplete }) => { - const styles = useStyles(); - const { data: migrationsStats, isLoading } = useLatestStats(); - - const [isFlyoutOpen, setIsFlyoutOpen] = useState(); - const [flyoutMigrationStats, setFlyoutMigrationStats] = useState< - RuleMigrationTaskStats | undefined - >(); - - const closeFlyout = useCallback(() => { - setIsFlyoutOpen(false); - setFlyoutMigrationStats(undefined); - }, []); - - const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => { - setFlyoutMigrationStats(migrationStats); - setIsFlyoutOpen(true); - }, []); - - return ( - - - {isLoading ? ( - - ) : ( - +export const StartMigrationCard: OnboardingCardComponent = React.memo( + ({ checkComplete, isCardComplete, setExpandedCardId }) => { + const styles = useStyles(); + const { data: migrationsStats, isLoading } = useLatestStats(); + + const [isFlyoutOpen, setIsFlyoutOpen] = useState(); + const [flyoutMigrationStats, setFlyoutMigrationStats] = useState< + RuleMigrationTaskStats | undefined + >(); + + const closeFlyout = useCallback(() => { + setIsFlyoutOpen(false); + setFlyoutMigrationStats(undefined); + if (!isCardComplete(OnboardingCardId.siemMigrationsStart)) { + checkComplete(); + } + }, [checkComplete, isCardComplete]); + + const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => { + setFlyoutMigrationStats(migrationStats); + setIsFlyoutOpen(true); + }, []); + + if (!isCardComplete(OnboardingCardId.siemMigrationsAiConnectors)) { + return ( + + setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors) + } + /> + ); + } + + return ( + + + {isLoading ? ( + + ) : ( + + )} + + +

{i18n.START_MIGRATION_CARD_FOOTER_NOTE}

+
+
+ {isFlyoutOpen && ( + )} -
- {isFlyoutOpen && ( - - )} -
- ); -}; + + ); + } +); +StartMigrationCard.displayName = 'StartMigrationCard'; // eslint-disable-next-line import/no-default-export export default StartMigrationCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index ebd495eb2506b..09b6de096a5da 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -11,6 +11,25 @@ export const START_MIGRATION_CARD_TITLE = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.title', { defaultMessage: 'Translate your existing SIEM Rules to Elastic' } ); +export const START_MIGRATION_CARD_FOOTER_NOTE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.footerNote', + { + defaultMessage: + 'Splunk and related marks are trademarks or registered trademarks of Splunk LLC in the United States and other countries.', + } +); +export const START_MIGRATION_CARD_CONNECTOR_MISSING_TEXT = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.connectorMissingText', + { + defaultMessage: 'Rule migrations require an AI connector to be configured.', + } +); +export const START_MIGRATION_CARD_CONNECTOR_MISSING_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.connectorMissingText', + { + defaultMessage: 'AI provider step', + } +); export const START_MIGRATION_CARD_UPLOAD_TITLE = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.upload.title', diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts index bc118ee3b1518..aa331bf17c832 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts @@ -20,7 +20,7 @@ export const SPL_RULES_COLUMNS = [ 'action.correlationsearch.annotations', ] as const; -export const RULES_SPL_QUERY = `| rest splunk_server=local /servicesNS/-/-/saved/searches +export const RULES_SPLUNK_QUERY = `| rest /servicesNS/-/-/saved/searches | search action.correlationsearch.enabled = "1" OR (eai:acl.app = "Splunk_Security_Essentials" AND is_scheduled=1) | where disabled=0 | table ${SPL_RULES_COLUMNS.join(', ')}`; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx index 863dd3864c925..11fb88a1cade2 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { RULES_SPL_QUERY } from '../../../../constants'; +import { RULES_SPLUNK_QUERY } from '../../../../constants'; import * as i18n from './translations'; interface CopyExportQueryProps { @@ -33,7 +33,7 @@ export const CopyExportQuery = React.memo(({ onCopied }) =
{/* onCopy react event is dispatched when the user copies text manually */} - {RULES_SPL_QUERY} + {RULES_SPLUNK_QUERY}
diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts index be90d3b9e3bb1..7562f67460dc8 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts @@ -62,7 +62,7 @@ const convertFormat = (row: SplunkRow): OriginalRule => { title: row.result.title, query: row.result.search, query_language: 'spl', - description: row.result['action.escu.eli5'] || row.result.description, + description: row.result['action.escu.eli5']?.trim() || row.result.description, }; if (row.result['action.correlationsearch.annotations']) { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/translations.ts new file mode 100644 index 0000000000000..936bc07e6576e --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/translations.ts @@ -0,0 +1,17 @@ +/* + * 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 RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createSuccess', + { defaultMessage: 'Rules uploaded successfully' } +); +export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createError', + { defaultMessage: 'Failed to upload rules file' } +); From 0d6ae1fd392763409f28a94ebb6fb2e78079049a Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:23:56 +0000 Subject: [PATCH 16/48] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../security_solution/public/siem_migrations/rules/api/api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts index 0ce1adac01496..0675a388dd9fc 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -19,7 +19,6 @@ import { SIEM_RULE_MIGRATION_START_PATH, SIEM_RULE_MIGRATION_STATS_PATH, SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, - SIEM_RULE_MIGRATION_CREATE_PATH, } from '../../../../common/siem_migrations/constants'; import type { CreateRuleMigrationRequestBody, From 6e63ffa79a4a41c67d7533b8398efa86d70ce1a9 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 4 Dec 2024 15:41:23 +0100 Subject: [PATCH 17/48] add langsmith trace options --- .../start_migration_check_complete.ts | 2 +- .../public/siem_migrations/rules/api/api.ts | 7 +- .../service/hooks/use_create_migration.ts | 2 +- .../rules/service/rule_migrations_service.ts | 83 ++++++++++++------- .../siem_migrations/rules/service/storage.ts | 30 +++++-- 5 files changed, 78 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts index 9c98ef0f27da5..41e65352d4bc3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts @@ -10,7 +10,7 @@ import type { OnboardingCardCheckComplete } from '../../../../../types'; export const checkStartMigrationCardComplete: OnboardingCardCheckComplete = async ({ siemMigrations, }) => { - const migrationsStats = await siemMigrations.rules.getRuleMigrationTasksStats(); + const migrationsStats = await siemMigrations.rules.getRuleMigrationsStats(); const isComplete = migrationsStats.length > 0; return isComplete; }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts index 0ce1adac01496..db6f0117d4a77 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -19,7 +19,6 @@ import { SIEM_RULE_MIGRATION_START_PATH, SIEM_RULE_MIGRATION_STATS_PATH, SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, - SIEM_RULE_MIGRATION_CREATE_PATH, } from '../../../../common/siem_migrations/constants'; import type { CreateRuleMigrationRequestBody, @@ -33,17 +32,17 @@ import type { GetRuleMigrationStatsResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; -export interface GetRuleMigrationsStatsParams { +export interface GetRuleMigrationStatsParams { /** `id` of the migration to get stats for */ migrationId: string; /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; } /** Retrieves the stats for all the existing migrations, aggregated by `migration_id`. */ -export const getRuleMigrationsStats = async ({ +export const getRuleMigrationStats = async ({ migrationId, signal, -}: GetRuleMigrationsStatsParams): Promise => { +}: GetRuleMigrationStatsParams): Promise => { return KibanaServices.get().http.get( replaceParams(SIEM_RULE_MIGRATION_STATS_PATH, { migration_id: migrationId }), { version: '1', signal } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts index f5cc2d66f819c..94082cf59d359 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts @@ -34,7 +34,7 @@ export const useCreateMigration = (onSuccess: OnSuccess) => { try { dispatch({ type: 'start' }); const migrationId = await siemMigrations.rules.createRuleMigration(data); - const stats = await siemMigrations.rules.getRuleMigrationsStats(migrationId); + const stats = await siemMigrations.rules.getRuleMigrationStats(migrationId); notifications.toasts.addSuccess(RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS); onSuccess(stats); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index ea762f000fdd2..5e5dded3bb691 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -7,6 +7,12 @@ import { BehaviorSubject, type Observable } from 'rxjs'; import type { CoreStart } from '@kbn/core/public'; +import type { TraceOptions } from '@kbn/elastic-assistant/impl/assistant/types'; +import { + DEFAULT_ASSISTANT_NAMESPACE, + TRACE_OPTIONS_SESSION_STORAGE_KEY, +} from '@kbn/elastic-assistant/impl/assistant_context/constants'; +import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen'; import type { RuleMigrationTaskStats } from '../../../../common/siem_migrations/model/rule_migration.gen'; import type { CreateRuleMigrationRequestBody, @@ -20,7 +26,7 @@ import { licenseService } from '../../../common/hooks/use_license'; import type { GetRuleMigrationsStatsAllParams } from '../api/api'; import { createRuleMigration, - getRuleMigrationsStats, + getRuleMigrationStats, getRuleMigrationsStatsAll, startRuleMigration, } from '../api/api'; @@ -29,13 +35,21 @@ import { getSuccessToast } from './success_notification'; import { RuleMigrationsStorage } from './storage'; import * as i18n from './translations'; +// use the default assistant namespace since it's the only one we use +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 CREATE_MIGRATION_BODY_BATCH_SIZE = 50 as const; export class SiemRulesMigrationsService { private readonly latestStats$: BehaviorSubject; private isPolling = false; - public connectorIdStorage = new RuleMigrationsStorage('connectorId'); + public connectorIdStorage = new RuleMigrationsStorage('connectorId'); + public traceOptionsStorage = new RuleMigrationsStorage('traceOptions', { + customKey: NAMESPACE_TRACE_OPTIONS_SESSION_STORAGE_KEY, + storageType: 'session', + }); constructor( private readonly core: CoreStart, @@ -71,23 +85,7 @@ export class SiemRulesMigrationsService { }); } - public async getRuleMigrationTasksStats( - params: GetRuleMigrationsStatsAllParams = {} - ): Promise { - const allStats = await this.getRuleMigrationsTaskStatsAll(params); - const results = allStats.map( - // the array order (by creation) is guaranteed by the API - (stats, index) => ({ ...stats, number: index + 1 } as RuleMigrationStats) // needs cast because of the `status` enum override - ); - this.latestStats$.next(results); // Always update the latest stats - return results; - } - public async createRuleMigration(body: CreateRuleMigrationRequestBody): Promise { - const connectorId = this.connectorIdStorage.get(); - if (!connectorId) { - throw new Error(i18n.MISSING_CONNECTOR_ERROR); - } if (body.length === 0) { throw new Error(i18n.EMPTY_RULES_ERROR); } @@ -101,22 +99,43 @@ export class SiemRulesMigrationsService { return migrationId as string; } - public async getRuleMigrationsStats(migrationId: string): Promise { - return getRuleMigrationsStats({ migrationId }); - } - public async startRuleMigration(migrationId: string): Promise { const connectorId = this.connectorIdStorage.get(); if (!connectorId) { throw new Error(i18n.MISSING_CONNECTOR_ERROR); } - // TODO: add langsmith options from local storage - const result = await startRuleMigration({ migrationId, connectorId }); + + const langSmithSettings = this.traceOptionsStorage.get(); + let langSmithOptions: LangSmithOptions | undefined; + if (langSmithSettings) { + langSmithOptions = { + project_name: langSmithSettings.langSmithProject, + api_key: langSmithSettings.langSmithApiKey, + }; + } + + const result = await startRuleMigration({ migrationId, connectorId, langSmithOptions }); this.startPolling(); return result; } - public async getRuleMigrationsTaskStatsAll( + public async getRuleMigrationStats(migrationId: string): Promise { + return getRuleMigrationStats({ migrationId }); + } + + public async getRuleMigrationsStats( + params: GetRuleMigrationsStatsAllParams = {} + ): Promise { + const allStats = await this.getRuleMigrationsStatsWithRetry(params); + const results = allStats.map( + // the array order (by creation) is guaranteed by the API + (stats, index) => ({ ...stats, number: index + 1 } as RuleMigrationStats) // needs cast because of the `status` enum override + ); + this.latestStats$.next(results); // Always update the latest stats + return results; + } + + private async getRuleMigrationsStatsWithRetry( params: GetRuleMigrationsStatsAllParams = {}, sleepSecs?: number ): Promise { @@ -125,23 +144,23 @@ export class SiemRulesMigrationsService { } return getRuleMigrationsStatsAll(params).catch((e) => { - // Retry only on network errors and 503s, otherwise throw - if (e.message !== 'Failed to fetch' && e.status !== 503) { + // Retry only on network errors (no status) and 503s, otherwise throw + if (e.status && e.status !== 503) { throw e; } - const nextSleepSecs = sleepSecs ? sleepSecs * 3 : 1; - if (nextSleepSecs > 60 * 2) { - // Wait for 2 minutes max for the API to be available again + const nextSleepSecs = sleepSecs ? sleepSecs * 2 : 1; // Exponential backoff + if (nextSleepSecs > 60) { + // Wait for a minutes max (two minutes total) for the API to be available again throw e; } - return this.getRuleMigrationsTaskStatsAll(params, nextSleepSecs); + return this.getRuleMigrationsStatsWithRetry(params, nextSleepSecs); }); } private async startTaskStatsPolling(): Promise { let pendingMigrationIds: string[] = []; do { - const results = await this.getRuleMigrationTasksStats(); + const results = await this.getRuleMigrationsStats(); if (pendingMigrationIds.length > 0) { // send notifications for finished migrations diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts index bbf53ec3a5404..874f1b05dfab6 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts @@ -7,23 +7,37 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; -export class RuleMigrationsStorage { - private readonly storage = new Storage(localStorage); +const storages = { + local: new Storage(localStorage), + session: new Storage(sessionStorage), +} as const; + +interface Options { + customKey?: string; + storageType?: keyof typeof storages; +} + +export class RuleMigrationsStorage { + private readonly storage: Storage; public key: string; - constructor(private readonly objectName: string, spaceId?: string) { - this.key = this.getStorageKey(spaceId); + constructor(private readonly objectName: string, private readonly options?: Options) { + this.storage = storages[this.options?.storageType ?? 'local']; + this.key = this.getKey(); } - private getStorageKey(spaceId: string = 'default') { + private getKey(spaceId: string = 'default'): string { + if (this.options?.customKey) { + return this.options.customKey; + } return `siem_migrations.rules.${this.objectName}.${spaceId}`; } public setSpaceId(spaceId: string) { - this.key = this.getStorageKey(spaceId); + this.key = this.getKey(spaceId); } - public get = () => this.storage.get(this.key); - public set = (value: string) => this.storage.set(this.key, value); + public get = (): T | undefined => this.storage.get(this.key); + public set = (value: T) => this.storage.set(this.key, value); public remove = () => this.storage.remove(this.key); } From ac6e675494ddf1026441595ffe4f0920582c11db Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 4 Dec 2024 15:49:43 +0100 Subject: [PATCH 18/48] lint fixes --- .../start_migration/panels/migration_result_panel.tsx | 8 ++++---- .../siem_migrations/start_migration/translations.ts | 11 +++-------- .../rules/service/success_notification.tsx | 6 +++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx index 4b8fca56ac5b9..b73b3cc8b4921 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx @@ -38,10 +38,10 @@ export const MigrationResultPanel = React.memo(({ mig

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

diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index 09b6de096a5da..bdb3f31842549 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -26,9 +26,7 @@ export const START_MIGRATION_CARD_CONNECTOR_MISSING_TEXT = i18n.translate( ); export const START_MIGRATION_CARD_CONNECTOR_MISSING_BUTTON = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.connectorMissingText', - { - defaultMessage: 'AI provider step', - } + { defaultMessage: 'AI provider step' } ); export const START_MIGRATION_CARD_UPLOAD_TITLE = i18n.translate( @@ -103,13 +101,10 @@ export const START_MIGRATION_CARD_RESULT_TITLE = (number: number) => values: { number }, }); -export const START_MIGRATION_CARD_RESULT_DESCRIPTION = (values: { - createdAt: string; - finishedAt: string; -}) => +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, + values: { createdAt, finishedAt }, }); export const VIEW_TRANSLATED_RULES_TITLE = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx index 830e3c5f4a531..f87755943f830 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx @@ -17,9 +17,9 @@ import type { ToastInput } from '@kbn/core-notifications-browser'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { RuleMigrationTask } from '../types'; +import type { RuleMigrationStats } from '../types'; -export const getSuccessToast = (migration: RuleMigrationTask, core: CoreStart): ToastInput => ({ +export const getSuccessToast = (migration: RuleMigrationStats, core: CoreStart): ToastInput => ({ color: 'success', iconType: 'check', toastLifeTimeMs: 1000 * 60 * 30, // 30 minutes @@ -34,7 +34,7 @@ export const getSuccessToast = (migration: RuleMigrationTask, core: CoreStart): ), }); -const SuccessToastContent: React.FC<{ migration: RuleMigrationTask }> = ({ migration }) => { +const SuccessToastContent: React.FC<{ migration: RuleMigrationStats }> = ({ migration }) => { const navigation = { deepLinkId: SecurityPageName.siemMigrationsRules, path: migration.id }; const { navigateTo, getAppUrl } = useNavigation(); From dcaffe2ba6c05c9b071c8575aa711b209110071c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:14:22 +0000 Subject: [PATCH 19/48] [CI] Auto-commit changed files from 'yarn openapi:generate' --- .../common/api/quickstart_client.gen.ts | 4 +++- .../services/security_solution_api.gen.ts | 13 +++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index b03a81b3b2249..0547da470152b 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -349,6 +349,7 @@ import type { ResolveTimelineResponse, } from './timeline/resolve_timeline/resolve_timeline_route.gen'; import type { + CreateRuleMigrationRequestParamsInput, CreateRuleMigrationRequestBodyInput, CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, @@ -679,7 +680,7 @@ If a record already exists for the specified entity, that record is overwritten this.log.info(`${new Date().toISOString()} Calling API CreateRuleMigration`); return this.kbnClient .request({ - path: '/internal/siem_migrations/rules', + path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, @@ -2233,6 +2234,7 @@ export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } export interface CreateRuleMigrationProps { + params: CreateRuleMigrationRequestParamsInput; body: CreateRuleMigrationRequestBodyInput; } export interface CreateTimelinesProps { diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 1e97d22454f37..86e32be5a6740 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -32,7 +32,10 @@ import { CopyTimelineRequestBodyInput } from '@kbn/security-solution-plugin/comm import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen'; import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; import { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen'; -import { CreateRuleMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + CreateRuleMigrationRequestParamsInput, + CreateRuleMigrationRequestBodyInput, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { CreateTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/create_timelines/create_timelines_route.gen'; import { CreateUpdateProtectionUpdatesNoteRequestParamsInput, @@ -369,7 +372,12 @@ If a record already exists for the specified entity, that record is overwritten */ createRuleMigration(props: CreateRuleMigrationProps, kibanaSpace: string = 'default') { return supertest - .post(routeWithNamespace('/internal/siem_migrations/rules', kibanaSpace)) + .post( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + kibanaSpace + ) + ) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') @@ -1569,6 +1577,7 @@ export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } export interface CreateRuleMigrationProps { + params: CreateRuleMigrationRequestParamsInput; body: CreateRuleMigrationRequestBodyInput; } export interface CreateTimelinesProps { From 246046254fdb6a0f8e7d283e8a0f20cd5557945d Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 5 Dec 2024 10:19:30 +0100 Subject: [PATCH 20/48] resources missing route --- .../common/siem_migrations/constants.ts | 2 + .../model/api/rules/rule_migration.gen.ts | 24 ++++++++ .../api/rules/rule_migration.schema.yaml | 36 ++++++++++++ .../lib/siem_migrations/rules/api/index.ts | 2 + .../rules/api/resources/missing.ts | 58 +++++++++++++++++++ .../rules/data/rule_migrations_data_client.ts | 52 +++++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts diff --git a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts index fadbd2a2b173d..b6dff672d28d4 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts @@ -25,6 +25,8 @@ export const SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH = `${SIEM_RULE_MIGRATION_PATH}/install_translated` as const; export const SIEM_RULE_MIGRATION_RESOURCES_PATH = `${SIEM_RULE_MIGRATION_PATH}/resources` as const; +export const SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH = + `${SIEM_RULE_MIGRATION_RESOURCES_PATH}/missing` as const; export enum SiemMigrationTaskStatus { READY = 'ready', diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 529070cc32ae8..11709518b9e9c 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -99,6 +99,30 @@ export type GetRuleMigrationResourcesRequestParamsInput = z.input< export type GetRuleMigrationResourcesResponse = z.infer; export const GetRuleMigrationResourcesResponse = z.array(RuleMigrationResource); +export type GetRuleMigrationResourcesMissingRequestParams = z.infer< + typeof GetRuleMigrationResourcesMissingRequestParams +>; +export const GetRuleMigrationResourcesMissingRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type GetRuleMigrationResourcesMissingRequestParamsInput = z.input< + typeof GetRuleMigrationResourcesMissingRequestParams +>; + +export type GetRuleMigrationResourcesMissingResponse = z.infer< + typeof GetRuleMigrationResourcesMissingResponse +>; +export const GetRuleMigrationResourcesMissingResponse = z.object({ + /** + * The macro resources missing + */ + macro: z.array(z.string()).optional(), + /** + * The list resources missing + */ + list: z.array(z.string()).optional(), +}); + export type GetRuleMigrationStatsRequestParams = z.infer; export const GetRuleMigrationStatsRequestParams = z.object({ migration_id: NonEmptyString, diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index 50e146e1e8f12..d98ce51357212 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -434,3 +434,39 @@ paths: type: array items: $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationResource' + + /internal/siem_migrations/rules/{migration_id}/resources/missing: + get: + summary: Gets missing rule migration resources for a migration + operationId: GetRuleMigrationResourcesMissing + x-codegen-enabled: true + x-internal: true + description: Identifies missing resources from all the rules of an existing SIEM rules migration + tags: + - SIEM Rule Migrations + - Resources + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to attach the resources + $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Indicates missing migration resources have been identified correctly + content: + application/json: + schema: + type: object + properties: + macro: + type: array + description: The macro resources missing + items: + type: string + list: + type: array + description: The list resources missing + items: + type: string diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts index c6f3c51a1bb53..8ccbbe505fa30 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts @@ -20,6 +20,7 @@ import { registerSiemRuleMigrationsResourceGetRoute } from './resources/get'; import { registerSiemRuleMigrationsRetryRoute } from './retry'; import { registerSiemRuleMigrationsInstallRoute } from './install'; import { registerSiemRuleMigrationsInstallTranslatedRoute } from './install_translated'; +import { registerSiemRuleMigrationsResourceGetMissingRoute } from './resources/missing'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, @@ -39,4 +40,5 @@ export const registerSiemRuleMigrationsRoutes = ( registerSiemRuleMigrationsResourceUpsertRoute(router, logger); registerSiemRuleMigrationsResourceGetRoute(router, logger); + registerSiemRuleMigrationsResourceGetMissingRoute(router, logger); }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts new file mode 100644 index 0000000000000..0e269d44b7507 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts @@ -0,0 +1,58 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { + GetRuleMigrationResourcesMissingRequestParams, + type GetRuleMigrationResourcesMissingResponse, +} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH } from '../../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { withLicense } from '../util/with_license'; + +export const registerSiemRuleMigrationsResourceGetMissingRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: buildRouteValidationWithZod(GetRuleMigrationResourcesMissingRequestParams), + }, + }, + }, + withLicense( + async ( + context, + req, + res + ): Promise> => { + const migrationId = req.params.migration_id; + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + + const resources = await ruleMigrationsClient.data.getMissingResources(migrationId); + + return res.ok({ body: resources }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts index 8960edd0cce21..225c682b435b0 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts @@ -6,6 +6,8 @@ */ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { RuleMigrationResourceType } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { getRuleResourceIdentifier } from '../../../../../common/siem_migrations/rules/resources'; import { RuleMigrationsDataIntegrationsClient } from './rule_migrations_data_integrations_client'; import { RuleMigrationsDataResourcesClient } from './rule_migrations_data_resources_client'; import { RuleMigrationsDataRulesClient } from './rule_migrations_data_rules_client'; @@ -44,4 +46,54 @@ export class RuleMigrationsDataClient { logger ); } + + public async getMissingResources( + migrationId: string + ): Promise> { + const missing: Record> = { + macro: new Set(), + list: new Set(), + }; + + const { data } = await this.rules.get(migrationId); + // This assumes all rules in the migration have the same vendor + const identifyRuleResources = getRuleResourceIdentifier(data[0].original_rule); + + // Identify all resources in the rules + for (const rule of data) { + const identifiedResources = identifyRuleResources(rule.original_rule.query); + for (const type of ['macro', 'list'] as const) { + for (const resource of identifiedResources[type]) { + missing[type].add(resource); + } + } + } + + // Identify all resources in the existing macros + const existingMacroResources = await this.resources.get(migrationId, 'macro'); + for (const macro of existingMacroResources) { + const nestedResourcesIdentified = identifyRuleResources(macro.content); + for (const type of ['macro', 'list'] as const) { + for (const resource of nestedResourcesIdentified[type]) { + missing[type].add(resource); + } + } + } + + // Exclude existing macros + for (const resource of existingMacroResources) { + missing.macro.delete(resource.name); + } + + // Exclude existing lists + const existingListResources = await this.resources.get(migrationId, 'list'); + for (const list of existingListResources) { + missing.list.delete(list.name); + } + + return { + macro: Array.from(missing.macro), + list: Array.from(missing.list), + }; + } } From 0957440039fe6dc642453d69dde652d7afac45a1 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 5 Dec 2024 10:20:19 +0100 Subject: [PATCH 21/48] organize get and filter parameters --- .../siem_migrations/start_migration/index.ts | 2 +- .../lib/siem_migrations/rules/api/get.ts | 14 +-- .../data/rule_migrations_data_rules_client.ts | 108 +++++++++--------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts index e2b8f5f7da0db..fcf950e0840e9 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts @@ -19,7 +19,7 @@ export const startMigrationCardConfig: OnboardingCardConfig = { Component: React.lazy( () => import( - /* webpackChunkName: "onboarding_siem_migrations_upload_rules_card" */ + /* webpackChunkName: "onboarding_siem_migrations_start_migration_card" */ './start_migration_card' ) ), diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts index 8ff8911c069ce..dd13a75cdf83a 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts @@ -14,6 +14,7 @@ import { } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; +import type { RuleMigrationGetOptions } from '../data/rule_migrations_data_rules_client'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsGetRoute = ( @@ -43,14 +44,13 @@ export const registerSiemRuleMigrationsGetRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const from = page && perPage ? page * perPage : 0; - const size = perPage; + const options: RuleMigrationGetOptions = { + filters: { searchTerm }, + size: perPage, + from: page && perPage ? page * perPage : 0, + }; - const result = await ruleMigrationsClient.data.rules.get( - { migrationId, searchTerm }, - from, - size - ); + const result = await ruleMigrationsClient.data.rules.get(migrationId, options); return res.ok({ body: result }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index f2a9d6b26aa2f..3f6fdf01ff8ab 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -38,34 +38,23 @@ export type UpdateRuleMigrationInput = { elastic_rule?: Partial } & export type RuleMigrationDataStats = Omit; export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; -export interface RuleMigrationFilterOptions { - migrationId: string; +export interface RuleMigrationFilters { status?: SiemMigrationStatus | SiemMigrationStatus[]; ids?: string[]; installable?: boolean; searchTerm?: string; } +export interface RuleMigrationGetOptions { + filters?: RuleMigrationFilters; + from?: number; + size?: number; +} /* BULK_MAX_SIZE defines the number to break down the bulk operations by. * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. */ const BULK_MAX_SIZE = 500 as const; /* The default number of rule migrations to retrieve in a single GET request. */ -const DEFAULT_GET_SIZE = 20 as const; - -const getInstallableConditions = (): QueryDslQueryContainer[] => { - return [ - { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } }, - { - nested: { - path: 'elastic_rule', - query: { - bool: { must_not: { exists: { field: 'elastic_rule.id' } } }, - }, - }, - }, - ]; -}; export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { /** Indexes an array of rule migrations to be processed */ @@ -130,12 +119,11 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves an array of rule documents of a specific migrations */ async get( - filters: RuleMigrationFilterOptions, - from?: number, - size: number = DEFAULT_GET_SIZE + migrationId: string, + { filters = {}, from, size }: RuleMigrationGetOptions = {} ): Promise<{ total: number; data: StoredRuleMigration[] }> { const index = await this.getIndexName(); - const query = this.getFilterQuery(filters); + const query = this.getFilterQuery(migrationId, { ...filters }); const result = await this.esClient .search({ index, query, sort: '_doc', from, size }) @@ -157,7 +145,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient */ async takePending(migrationId: string, size: number): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery({ migrationId, status: SiemMigrationStatus.PENDING }); + const query = this.getFilterQuery(migrationId, { status: SiemMigrationStatus.PENDING }); const storedRuleMigrations = await this.esClient .search({ index, query, sort: '_doc', size }) @@ -236,7 +224,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { refresh = false }: { refresh?: boolean } = {} ): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery({ migrationId, status: statusToQuery }); + const query = this.getFilterQuery(migrationId, { status: statusToQuery }); const script = { source: `ctx._source['status'] = '${statusToUpdate}'` }; await this.esClient.updateByQuery({ index, query, script, refresh }).catch((error) => { this.logger.error(`Error updating rule migrations status: ${error.message}`); @@ -247,24 +235,11 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves the translation stats for the rule migrations with the provided id */ async getTranslationStats(migrationId: string): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery({ migrationId }); + const query = this.getFilterQuery(migrationId); const aggregations = { - prebuilt: { - filter: { - nested: { - path: 'elastic_rule', - query: { exists: { field: 'elastic_rule.prebuilt_rule_id' } }, - }, - }, - }, - installable: { - filter: { - bool: { - must: getInstallableConditions(), - }, - }, - }, + prebuilt: { filter: conditions.isPrebuilt() }, + installable: { filter: { bool: { must: conditions.isInstallable() } } }, }; const result = await this.esClient .search({ index, query, aggregations, _source: false }) @@ -290,7 +265,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves the stats for the rule migrations with the provided id */ async getStats(migrationId: string): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery({ migrationId }); + const query = this.getFilterQuery(migrationId); const aggregations = { pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } }, processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, @@ -360,13 +335,10 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient })); } - private getFilterQuery({ - migrationId, - status, - ids, - installable, - searchTerm, - }: RuleMigrationFilterOptions): QueryDslQueryContainer { + private getFilterQuery( + migrationId: string, + { status, ids, installable, searchTerm }: RuleMigrationFilters = {} + ): QueryDslQueryContainer { const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }]; if (status) { if (Array.isArray(status)) { @@ -379,16 +351,44 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient filter.push({ terms: { _id: ids } }); } if (installable) { - filter.push(...getInstallableConditions()); + filter.push(...conditions.isInstallable()); } if (searchTerm?.length) { - filter.push({ - nested: { - path: 'elastic_rule', - query: { match: { 'elastic_rule.title': searchTerm } }, - }, - }); + filter.push(conditions.matchTitle(searchTerm)); } return { bool: { filter } }; } } + +const conditions = { + isFullyTranslated(): QueryDslQueryContainer { + return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } }; + }, + isInstalled(): QueryDslQueryContainer { + return { + nested: { + path: 'elastic_rule', + query: { bool: { must_not: { exists: { field: 'elastic_rule.id' } } } }, + }, + }; + }, + isPrebuilt(): QueryDslQueryContainer { + return { + nested: { + path: 'elastic_rule', + query: { exists: { field: 'elastic_rule.prebuilt_rule_id' } }, + }, + }; + }, + matchTitle(title: string): QueryDslQueryContainer { + return { + nested: { + path: 'elastic_rule', + query: { match: { 'elastic_rule.title': title } }, + }, + }; + }, + isInstallable(): QueryDslQueryContainer[] { + return [this.isFullyTranslated(), this.isInstalled()]; + }, +}; From ca33b12f50fdd4f402464cbf55bb38a0b70d763e Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 5 Dec 2024 11:05:03 +0100 Subject: [PATCH 22/48] type fixes --- .../rules/components/header_buttons/index.tsx | 4 ++-- .../lib/siem_migrations/rules/api/util/installation.ts | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx index 728873f046d2e..3f255a49f87c2 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx @@ -10,13 +10,13 @@ import React, { useMemo } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import * as i18n from './translations'; -import type { RuleMigrationTask } from '../../types'; +import type { RuleMigrationStats } from '../../types'; export interface HeaderButtonsProps { /** * Available rule migrations stats */ - ruleMigrationsStats: RuleMigrationTask[]; + ruleMigrationsStats: RuleMigrationStats[]; /** * Selected rule migration id diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts index df86a1f953656..2fce95be9dafe 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts @@ -177,10 +177,8 @@ export const installTranslated = async ({ const detectionRulesClient = securitySolutionContext.getDetectionRulesClient(); const ruleMigrationsClient = securitySolutionContext.getSiemRuleMigrationsClient(); - const { data: rulesToInstall } = await ruleMigrationsClient.data.rules.get({ - migrationId, - ids, - installable: true, + const { data: rulesToInstall } = await ruleMigrationsClient.data.rules.get(migrationId, { + filters: { ids, installable: true }, }); const { customRulesToInstall, prebuiltRulesToInstall } = rulesToInstall.reduce( From 0af2d104fa6544d0e22632c3b2e759b9b9e73836 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 5 Dec 2024 11:06:25 +0100 Subject: [PATCH 23/48] typo --- .../rules/data/rule_migrations_data_rules_client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 3f6fdf01ff8ab..716d19ce16cdf 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -364,7 +364,7 @@ const conditions = { isFullyTranslated(): QueryDslQueryContainer { return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } }; }, - isInstalled(): QueryDslQueryContainer { + isNotInstalled(): QueryDslQueryContainer { return { nested: { path: 'elastic_rule', @@ -389,6 +389,6 @@ const conditions = { }; }, isInstallable(): QueryDslQueryContainer[] { - return [this.isFullyTranslated(), this.isInstalled()]; + return [this.isFullyTranslated(), this.isNotInstalled()]; }, }; From 349915bbdc4cb276cd3c11880f62e7e80bd024b5 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 5 Dec 2024 13:53:34 +0100 Subject: [PATCH 24/48] restore batch size --- .../siem_migrations/rules/task/rule_migrations_task_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index c8ce10dac05b9..a6ea5c9040e16 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -30,7 +30,7 @@ import { IntegrationRetriever } from './util/integration_retriever'; import { retrievePrebuiltRulesMap } from './util/prebuilt_rules'; import { RuleResourceRetriever } from './util/rule_resource_retriever'; -const ITERATION_BATCH_SIZE = 1 as const; +const ITERATION_BATCH_SIZE = 50 as const; const ITERATION_SLEEP_SECONDS = 10 as const; type MigrationsRunning = Map; From 9e548ffe9ce2b51faff564fbb57e75d4f20b550b Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 5 Dec 2024 15:35:35 +0100 Subject: [PATCH 25/48] use Security common NonEmptyString --- .../model/api/rules/rule_migration.gen.ts | 3 ++- .../api/rules/rule_migration.schema.yaml | 26 +++++++++---------- .../siem_migrations/model/common.gen.ts | 6 ----- .../siem_migrations/model/common.schema.yaml | 5 ---- .../model/rule_migration.gen.ts | 2 +- .../model/rule_migration.schema.yaml | 22 ++++++++-------- 6 files changed, 27 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 529070cc32ae8..8a549e8e11817 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -17,7 +17,7 @@ import { z } from '@kbn/zod'; import { ArrayFromString } from '@kbn/zod-helpers'; -import { NonEmptyString, ConnectorId, LangSmithOptions } from '../../common.gen'; +import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { ElasticRulePartial, RuleMigrationTranslationResult, @@ -30,6 +30,7 @@ import { RuleMigrationResourceType, RuleMigrationResource, } from '../../rule_migration.gen'; +import { ConnectorId, LangSmithOptions } from '../../common.gen'; export type CreateRuleMigrationRequestParams = z.infer; export const CreateRuleMigrationRequestParams = z.object({ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index 50e146e1e8f12..8b9d264cf4104 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -26,7 +26,7 @@ paths: properties: id: description: The rule migration id - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' elastic_rule: description: The migrated elastic rule attributes to update. $ref: '../../rule_migration.schema.yaml#/components/schemas/ElasticRulePartial' @@ -86,7 +86,7 @@ paths: required: false schema: description: The migration id to create rules for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' requestBody: required: true content: @@ -107,7 +107,7 @@ paths: properties: migration_id: description: The migration id created. - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' get: summary: Retrieves all the rules of a migration operationId: GetRuleMigration @@ -122,7 +122,7 @@ paths: required: true schema: description: The migration id to start - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - name: page in: query required: false @@ -174,7 +174,7 @@ paths: required: true schema: description: The migration id to install rules for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' requestBody: required: true content: @@ -183,7 +183,7 @@ paths: type: array items: description: The rule migration id - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates rules migrations have been installed correctly. @@ -212,7 +212,7 @@ paths: required: true schema: description: The migration id to install translated rules for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates rules migrations have been installed correctly. @@ -242,7 +242,7 @@ paths: required: true schema: description: The migration id to start - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' requestBody: required: true content: @@ -287,7 +287,7 @@ paths: required: true schema: description: The migration id to fetch stats for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates the migration stats has been retrieved correctly. @@ -312,7 +312,7 @@ paths: required: true schema: description: The migration id to fetch translation stats for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates the migration stats has been retrieved correctly. @@ -338,7 +338,7 @@ paths: required: true schema: description: The migration id to stop - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates migration task stop has been processed successfully. @@ -373,7 +373,7 @@ paths: required: true schema: description: The migration id to attach the resources - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' requestBody: required: true content: @@ -411,7 +411,7 @@ paths: required: true schema: description: The migration id to attach the resources - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - name: type in: query required: false diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts index 9b2cb12be785b..c6d0959cc10cf 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts @@ -16,12 +16,6 @@ import { z } from '@kbn/zod'; -/** - * A string that is not empty and does not contain only whitespace - */ -export type NonEmptyString = z.infer; -export const NonEmptyString = z.string().min(1).regex(/\S+/); - /** * The GenAI connector id to use. */ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml index 5799c092b777b..14a5160427f8a 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml @@ -6,11 +6,6 @@ paths: {} components: x-codegen-enabled: true schemas: - NonEmptyString: - type: string - pattern: \S+ - minLength: 1 - description: A string that is not empty and does not contain only whitespace ConnectorId: type: string description: The GenAI connector id to use. diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 4604e39613f30..b52cdb1c91f19 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -16,7 +16,7 @@ import { z } from '@kbn/zod'; -import { NonEmptyString } from './common.gen'; +import { NonEmptyString } from '../../api/model/primitives.gen'; /** * The original rule vendor identifier. diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 647f9ff62b729..4c88c66fc604d 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -36,13 +36,13 @@ components: properties: id: description: The original rule id. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' vendor: description: The original rule vendor identifier. $ref: '#/components/schemas/OriginalRuleVendor' title: description: The original rule name. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' description: type: string description: The original rule description. @@ -82,7 +82,7 @@ components: - esql prebuilt_rule_id: description: The Elastic prebuilt rule id matched. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' integration_ids: type: array items: @@ -90,7 +90,7 @@ components: description: The Elastic integration IDs related to the rule. id: description: The Elastic rule id installed as a result. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' ElasticRulePartial: description: The partial version of the migrated elastic rule. @@ -106,7 +106,7 @@ components: properties: id: description: The rule migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - $ref: '#/components/schemas/RuleMigrationData' RuleMigrationData: @@ -124,10 +124,10 @@ components: description: The moment of creation migration_id: description: The migration id. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' created_by: description: The username of the user who created the migration. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' original_rule: description: The original rule to migrate. $ref: '#/components/schemas/OriginalRule' @@ -163,7 +163,7 @@ components: properties: id: description: The migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' status: description: Indicates if the migration task status. $ref: '#/components/schemas/RuleMigrationTaskStatus' @@ -217,7 +217,7 @@ components: properties: id: description: The migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' rules: type: object description: The rules migration translation stats. @@ -303,10 +303,10 @@ components: properties: id: description: The rule resource migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' migration_id: description: The migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' updated_at: type: string description: The moment of the last update From aa06fe8347757d720538ff2877e677bb1025b0f5 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 5 Dec 2024 15:57:42 +0100 Subject: [PATCH 26/48] try/catch NDJson parser --- .../rules_file_upload/parse_rules_file.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts index 7562f67460dc8..3d5dbb32ccde8 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts @@ -33,23 +33,26 @@ const parseNDJSON = (fileContent: string): SplunkRow[] => { return fileContent .split(/\n(?=\{)/) // split at newline followed by '{'. .filter((entry) => entry.trim() !== '') // Remove empty entries. - .map((entry) => JSON.parse(entry)); // Parse each entry as JSON. + .map(parseJSON); // Parse each entry as JSON. }; const parseJSONArray = (fileContent: string): SplunkRow[] => { - let parsedContent: SplunkResult; + const parsedContent = parseJSON(fileContent); + if (!Array.isArray(parsedContent)) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_ARRAY); + } + return parsedContent; +}; + +const parseJSON = (fileContent: string) => { try { - parsedContent = JSON.parse(fileContent); + return JSON.parse(fileContent); } catch (error) { if (error instanceof RangeError) { throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); } throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_PARSE); } - if (!Array.isArray(parsedContent)) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_ARRAY); - } - return parsedContent; }; const convertFormat = (row: SplunkRow): OriginalRule => { From e8614d6d0f689e5b7aa35975f00158805db402aa Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Sat, 7 Dec 2024 19:24:01 +0100 Subject: [PATCH 27/48] adapt resources api and batch searchh --- .../model/api/rules/rule_migration.gen.ts | 16 +-- .../api/rules/rule_migration.schema.yaml | 28 ++-- .../siem_migrations/rules/resources/index.ts | 74 ++++++++-- .../rules/resources/splunk/index.ts | 19 +++ .../{ => splunk}/splunk_identifier.test.ts | 78 +++++++---- .../resources/splunk/splunk_identifier.ts | 56 ++++++++ .../rules/resources/splunk_identifier.ts | 48 ------- .../siem_migrations/rules/resources/types.ts | 18 ++- .../lib/siem_migrations/rules/api/create.ts | 16 +++ .../rules/api/resources/get.ts | 9 +- .../rules/api/resources/missing.ts | 6 +- .../rules/api/resources/upsert.ts | 22 ++- .../data/rule_migrations_data_base_client.ts | 53 +++++++- .../rules/data/rule_migrations_data_client.ts | 52 -------- .../rule_migrations_data_resources_client.ts | 103 ++++++++++++-- .../data/rule_migrations_data_rules_client.ts | 23 +++- .../task/util/rule_resource_retriever.ts | 126 +++++++++--------- 17 files changed, 494 insertions(+), 253 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/index.ts rename x-pack/plugins/security_solution/common/siem_migrations/rules/resources/{ => splunk}/splunk_identifier.test.ts (51%) create mode 100644 x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.ts delete mode 100644 x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk_identifier.ts diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 11709518b9e9c..aabd24cc11c5c 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -81,6 +81,8 @@ export type GetRuleMigrationResourcesRequestQuery = z.infer< export const GetRuleMigrationResourcesRequestQuery = z.object({ type: RuleMigrationResourceType.optional(), names: ArrayFromString(z.string()).optional(), + from: z.coerce.number().optional(), + size: z.coerce.number().optional(), }); export type GetRuleMigrationResourcesRequestQueryInput = z.input< typeof GetRuleMigrationResourcesRequestQuery @@ -109,19 +111,13 @@ export type GetRuleMigrationResourcesMissingRequestParamsInput = z.input< typeof GetRuleMigrationResourcesMissingRequestParams >; +/** + * The identified resources missing + */ export type GetRuleMigrationResourcesMissingResponse = z.infer< typeof GetRuleMigrationResourcesMissingResponse >; -export const GetRuleMigrationResourcesMissingResponse = z.object({ - /** - * The macro resources missing - */ - macro: z.array(z.string()).optional(), - /** - * The list resources missing - */ - list: z.array(z.string()).optional(), -}); +export const GetRuleMigrationResourcesMissingResponse = z.array(RuleMigrationResource); export type GetRuleMigrationStatsRequestParams = z.infer; export const GetRuleMigrationStatsRequestParams = z.object({ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index d98ce51357212..316f4dbf4df1e 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -425,6 +425,16 @@ paths: description: The names of the resource to retrieve items: type: string + - name: from + in: query + required: false + schema: + type: number + - name: size + in: query + required: false + schema: + type: number responses: 200: description: Indicates migration resources have been retrieved correctly @@ -454,19 +464,11 @@ paths: $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' responses: 200: - description: Indicates missing migration resources have been identified correctly + description: Indicates missing migration resources have been retrieved correctly content: application/json: schema: - type: object - properties: - macro: - type: array - description: The macro resources missing - items: - type: string - list: - type: array - description: The list resources missing - items: - type: string + type: array + description: The identified resources missing + items: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationResource' diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/index.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/index.ts index ffe4b3aca4076..8ec7adf050bf3 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/index.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/index.ts @@ -5,18 +5,72 @@ * 2.0. */ -import type { OriginalRule, OriginalRuleVendor } from '../../model/rule_migration.gen'; -import type { QueryResourceIdentifier, RuleResourceCollection } from './types'; -import { splResourceIdentifier } from './splunk_identifier'; +import type { + OriginalRule, + OriginalRuleVendor, + RuleMigrationResourceData, +} from '../../model/rule_migration.gen'; +import type { ResourceIdentifiers, RuleResource } from './types'; +import { splResourceIdentifiers } from './splunk'; -export const getRuleResourceIdentifier = (rule: OriginalRule): QueryResourceIdentifier => { - return ruleResourceIdentifiers[rule.vendor]; +const ruleResourceIdentifiers: Record = { + splunk: splResourceIdentifiers, }; -export const identifyRuleResources = (rule: OriginalRule): RuleResourceCollection => { - return getRuleResourceIdentifier(rule)(rule.query); +export const getRuleResourceIdentifier = (vendor: OriginalRuleVendor): ResourceIdentifiers => { + return ruleResourceIdentifiers[vendor]; }; -const ruleResourceIdentifiers: Record = { - splunk: splResourceIdentifier, -}; +export class ResourceIdentifier { + private identifiers: ResourceIdentifiers; + + constructor(vendor: OriginalRuleVendor) { + // The constructor may need query_language as an argument for other vendors + this.identifiers = ruleResourceIdentifiers[vendor]; + } + + public fromOriginalRule(originalRule: OriginalRule): RuleResource[] { + return this.identifiers.fromOriginalRule(originalRule); + } + + public fromResource(resource: RuleMigrationResourceData): RuleResource[] { + return this.identifiers.fromResource(resource); + } + + public fromOriginalRules(originalRules: OriginalRule[]): RuleResource[] { + const lists = new Set(); + const macros = new Set(); + originalRules.forEach((rule) => { + const resources = this.identifiers.fromOriginalRule(rule); + resources.forEach((resource) => { + if (resource.type === 'macro') { + macros.add(resource.name); + } else if (resource.type === 'list') { + lists.add(resource.name); + } + }); + }); + return [ + ...Array.from(macros).map((name) => ({ type: 'macro', name })), + ...Array.from(lists).map((name) => ({ type: 'list', name })), + ]; + } + + public fromResources(resources: RuleMigrationResourceData[]): RuleResource[] { + const lists = new Set(); + const macros = new Set(); + resources.forEach((resource) => { + this.identifiers.fromResource(resource).forEach((identifiedResource) => { + if (identifiedResource.type === 'macro') { + macros.add(identifiedResource.name); + } else if (identifiedResource.type === 'list') { + lists.add(identifiedResource.name); + } + }); + }); + return [ + ...Array.from(macros).map((name) => ({ type: 'macro', name })), + ...Array.from(lists).map((name) => ({ type: 'list', name })), + ]; + } +} diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/index.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/index.ts new file mode 100644 index 0000000000000..a16c328da947a --- /dev/null +++ b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/index.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 type { ResourceIdentifiers } from '../types'; +import { splResourceIdentifier } from './splunk_identifier'; + +export const splResourceIdentifiers: ResourceIdentifiers = { + fromOriginalRule: (originalRule) => splResourceIdentifier(originalRule.query), + fromResource: (resource) => { + if (resource.type === 'macro' && resource.content) { + return splResourceIdentifier(resource.content); + } + return []; + }, +}; diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk_identifier.test.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.test.ts similarity index 51% rename from x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk_identifier.test.ts rename to x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.test.ts index 2fa3be223aa67..6fb48353e315c 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk_identifier.test.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.test.ts @@ -13,48 +13,71 @@ describe('splResourceIdentifier', () => { '`macro_zero`, `macro_one(arg1)`, some search command `macro_two(arg1, arg2)` another command `macro_three(arg1, arg2, arg3)`'; const result = splResourceIdentifier(query); - - expect(result.macro).toEqual(['macro_zero', 'macro_one(1)', 'macro_two(2)', 'macro_three(3)']); - expect(result.list).toEqual([]); + expect(result).toEqual([ + { type: 'macro', name: 'macro_zero' }, + { type: 'macro', name: 'macro_one(1)' }, + { type: 'macro', name: 'macro_two(2)' }, + { type: 'macro', name: 'macro_three(3)' }, + ]); }); it('should extract lookup tables correctly', () => { const query = - 'search ... | lookup my_lookup_table field AS alias OUTPUT new_field | inputlookup other_lookup_list | lookup third_lookup'; + 'search ... | lookup my_lookup_table field AS alias OUTPUT new_field | lookup other_lookup_list | lookup third_lookup'; const result = splResourceIdentifier(query); - - expect(result.macro).toEqual([]); - expect(result.list).toEqual(['my_lookup_table', 'other_lookup_list', 'third_lookup']); + expect(result).toEqual([ + { type: 'list', name: 'my_lookup_table' }, + { type: 'list', name: 'other_lookup_list' }, + { type: 'list', name: 'third_lookup' }, + ]); }); it('should extract both macros and lookup tables correctly', () => { const query = - '`macro_one` some search command | lookup my_lookup_table field AS alias OUTPUT new_field | inputlookup other_lookup_list | lookup third_lookup'; + '`macro_one` some search command | lookup my_lookup_table field AS alias OUTPUT new_field | lookup other_lookup_list | lookup third_lookup'; const result = splResourceIdentifier(query); + expect(result).toEqual([ + { type: 'macro', name: 'macro_one' }, + { type: 'list', name: 'my_lookup_table' }, + { type: 'list', name: 'other_lookup_list' }, + { type: 'list', name: 'third_lookup' }, + ]); + }); - expect(result.macro).toEqual(['macro_one']); - expect(result.list).toEqual(['my_lookup_table', 'other_lookup_list', 'third_lookup']); + it('should extract lookup correctly when there are modifiers', () => { + const query = + 'lookup my_lookup_1 field AS alias OUTPUT new_field | lookup local=true my_lookup_2 | lookup update=true my_lookup_3 | lookup local=true update=true my_lookup_4 | lookup update=false local=true my_lookup_5'; + + const result = splResourceIdentifier(query); + expect(result).toEqual([ + { type: 'list', name: 'my_lookup_1' }, + { type: 'list', name: 'my_lookup_2' }, + { type: 'list', name: 'my_lookup_3' }, + { type: 'list', name: 'my_lookup_4' }, + { type: 'list', name: 'my_lookup_5' }, + ]); }); it('should return empty arrays if no macros or lookup tables are found', () => { const query = 'search | stats count'; const result = splResourceIdentifier(query); - - expect(result.macro).toEqual([]); - expect(result.list).toEqual([]); + expect(result).toEqual([]); }); it('should handle queries with both macros and lookup tables mixed with other commands', () => { const query = - 'search `macro_one` | `my_lookup_table` field AS alias myfakelookup new_field | inputlookup real_lookup_list | `third_macro`'; + 'search `macro_one` | `my_lookup_table` field AS alias myfakelookup new_field | lookup real_lookup_list | `third_macro`'; const result = splResourceIdentifier(query); - - expect(result.macro).toEqual(['macro_one', 'my_lookup_table', 'third_macro']); - expect(result.list).toEqual(['real_lookup_list']); + expect(result).toEqual([ + { type: 'macro', name: 'macro_one' }, + { type: 'macro', name: 'my_lookup_table' }, + { type: 'macro', name: 'third_macro' }, + { type: 'list', name: 'real_lookup_list' }, + ]); }); it('should ignore macros or lookup tables inside string literals with double quotes', () => { @@ -62,9 +85,10 @@ describe('splResourceIdentifier', () => { '`macro_one` | lookup my_lookup_table | search title="`macro_two` and lookup another_table"'; const result = splResourceIdentifier(query); - - expect(result.macro).toEqual(['macro_one']); - expect(result.list).toEqual(['my_lookup_table']); + expect(result).toEqual([ + { type: 'macro', name: 'macro_one' }, + { type: 'list', name: 'my_lookup_table' }, + ]); }); it('should ignore macros or lookup tables inside string literals with single quotes', () => { @@ -72,9 +96,10 @@ describe('splResourceIdentifier', () => { "`macro_one` | lookup my_lookup_table | search title='`macro_two` and lookup another_table'"; const result = splResourceIdentifier(query); - - expect(result.macro).toEqual(['macro_one']); - expect(result.list).toEqual(['my_lookup_table']); + expect(result).toEqual([ + { type: 'macro', name: 'macro_one' }, + { type: 'list', name: 'my_lookup_table' }, + ]); }); it('should ignore macros or lookup tables inside comments wrapped by ```', () => { @@ -82,8 +107,9 @@ describe('splResourceIdentifier', () => { '`macro_one` | ```this is a comment with `macro_two` and lookup another_table``` lookup my_lookup_table'; const result = splResourceIdentifier(query); - - expect(result.macro).toEqual(['macro_one']); - expect(result.list).toEqual(['my_lookup_table']); + expect(result).toEqual([ + { type: 'macro', name: 'macro_one' }, + { type: 'list', name: 'my_lookup_table' }, + ]); }); }); diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.ts new file mode 100644 index 0000000000000..0fafbd7caf406 --- /dev/null +++ b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.ts @@ -0,0 +1,56 @@ +/* + * 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. + */ + +/** + * Important: + * This library uses regular expressions that are executed against arbitrary user input, they need to be safe from ReDoS attacks. + * Please make sure to test all regular expressions them before using them. + * At the time of writing, this tool can be used to test it: https://devina.io/redos-checker + */ + +import type { ResourceIdentifier, RuleResource } from '../types'; + +const listRegex = /\b(?:lookup)\s+([\w-]+)\b/g; // Captures only the lookup name +const macrosRegex = /`([\w-]+)(?:\(([^`]*?)\))?`/g; // Captures only the macro name and arguments + +export const splResourceIdentifier: ResourceIdentifier = (input) => { + // sanitize the query to avoid mismatching macro and list names inside comments or literal strings + const sanitizedInput = sanitizeInput(input); + + const resources: RuleResource[] = []; + let macroMatch; + while ((macroMatch = macrosRegex.exec(sanitizedInput)) !== null) { + const macroName = macroMatch[1] as string; + const args = macroMatch[2] as string; // This captures the content inside the parentheses + const argCount = args ? args.split(',').length : 0; // Count arguments if present + const macroWithArgs = argCount > 0 ? `${macroName}(${argCount})` : macroName; + resources.push({ type: 'macro', name: macroWithArgs }); + } + + let listMatch; + while ((listMatch = listRegex.exec(sanitizedInput)) !== null) { + resources.push({ type: 'list', name: listMatch[1] }); + } + + return resources; +}; + +// Comments should be removed before processing the query to avoid matching macro and list names inside them +const commentRegex = /```.*```/g; +// Literal strings should be replaced with a placeholder to avoid matching macro and list names inside them +const doubleQuoteStrRegex = /".*"/g; +const singleQuoteStrRegex = /'.*'/g; +// lookup operator can have modifiers like local=true or update=false before the lookup name, we need to remove them +const lookupModifiers = /\blookup\b\s+((local|update)=\s*(?:true|false)\s*)+/gi; + +const sanitizeInput = (query: string) => { + return query + .replaceAll(commentRegex, '') + .replaceAll(doubleQuoteStrRegex, '"literal"') + .replaceAll(singleQuoteStrRegex, "'literal'") + .replaceAll(lookupModifiers, 'lookup '); +}; diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk_identifier.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk_identifier.ts deleted file mode 100644 index fa46fff941c6b..0000000000000 --- a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk_identifier.ts +++ /dev/null @@ -1,48 +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. - */ - -/** - * Important: - * This library uses regular expressions that are executed against arbitrary user input, they need to be safe from ReDoS attacks. - * Please make sure to test them before using them in production. - * At the time of writing, this tool can be used to test it: https://devina.io/redos-checker - */ - -import type { QueryResourceIdentifier } from './types'; - -const listRegex = /\b(?:lookup|inputlookup)\s+([\w-]+)\b/g; // Captures only the lookup table name -const macrosRegex = /`([\w-]+)(?:\(([^`]*?)\))?`/g; // Captures only the macro name and arguments - -const commentRegex = /```.*```/g; -const doubleQuoteStrRegex = /".*"/g; -const singleQuoteStrRegex = /'.*'/g; - -export const splResourceIdentifier: QueryResourceIdentifier = (query) => { - // sanitize the query to avoid mismatching macro and list names inside comments or literal strings - const sanitizedQuery = query - .replaceAll(commentRegex, '') - .replaceAll(doubleQuoteStrRegex, '"literal"') - .replaceAll(singleQuoteStrRegex, "'literal'"); - - const macro = []; - let macroMatch; - while ((macroMatch = macrosRegex.exec(sanitizedQuery)) !== null) { - const macroName = macroMatch[1]; - const args = macroMatch[2]; // This captures the content inside the parentheses - const argCount = args ? args.split(',').length : 0; // Count arguments if present - const macroWithArgs = argCount > 0 ? `${macroName}(${argCount})` : macroName; - macro.push(macroWithArgs); - } - - const list = []; - let listMatch; - while ((listMatch = listRegex.exec(sanitizedQuery)) !== null) { - list.push(listMatch[1]); - } - - return { macro, list }; -}; diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/types.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/types.ts index 93f6f3ad3db17..70c6e3b72124f 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/types.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/types.ts @@ -5,7 +5,19 @@ * 2.0. */ -import type { RuleMigrationResourceType } from '../../model/rule_migration.gen'; +import type { + OriginalRule, + RuleMigrationResourceData, + RuleMigrationResourceType, +} from '../../model/rule_migration.gen'; -export type RuleResourceCollection = Record; -export type QueryResourceIdentifier = (query: string) => RuleResourceCollection; +export interface RuleResource { + type: RuleMigrationResourceType; + name: string; +} +export type ResourceIdentifier = (input: string) => RuleResource[]; + +export interface ResourceIdentifiers { + fromOriginalRule: (originalRule: OriginalRule) => RuleResource[]; + fromResource: (resource: RuleMigrationResourceData) => RuleResource[]; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts index 7c94631be6b65..279dde913ef71 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts @@ -8,6 +8,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { v4 as uuidV4 } from 'uuid'; +import { ResourceIdentifier } from '../../../../../common/siem_migrations/rules/resources'; import { SIEM_RULE_MIGRATION_CREATE_PATH } from '../../../../../common/siem_migrations/constants'; import { CreateRuleMigrationRequestBody, @@ -43,6 +44,11 @@ export const registerSiemRuleMigrationsCreateRoute = ( const originalRules = req.body; const migrationId = req.params.migration_id ?? uuidV4(); try { + const [firstOriginalRule] = originalRules; + if (!firstOriginalRule) { + return res.noContent(); + } + const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); @@ -53,6 +59,16 @@ export const registerSiemRuleMigrationsCreateRoute = ( await ruleMigrationsClient.data.integrations.create(); await ruleMigrationsClient.data.rules.create(ruleMigrations); + // Create identified resource documents without content to keep track of them + const resourceIdentifier = new ResourceIdentifier(firstOriginalRule.vendor); + const resources = resourceIdentifier + .fromOriginalRules(originalRules) + .map((resource) => ({ ...resource, migration_id: migrationId })); + + if (resources.length > 0) { + await ruleMigrationsClient.data.resources.create(resources); + } + return res.ok({ body: { migration_id: migrationId } }); } catch (err) { logger.error(err); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts index 7f2cfc8743f07..8d1e1d353e32d 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts @@ -39,16 +39,13 @@ export const registerSiemRuleMigrationsResourceGetRoute = ( withLicense( async (context, req, res): Promise> => { const migrationId = req.params.migration_id; - const { type, names } = req.query; + const { type, names, from, size } = req.query; try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const resources = await ruleMigrationsClient.data.resources.get( - migrationId, - type, - names - ); + const options = { filters: { type, names }, from, size }; + const resources = await ruleMigrationsClient.data.resources.get(migrationId, options); return res.ok({ body: resources }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts index 0e269d44b7507..84ea909c06fb4 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts @@ -45,9 +45,11 @@ export const registerSiemRuleMigrationsResourceGetMissingRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const resources = await ruleMigrationsClient.data.getMissingResources(migrationId); + const options = { filters: { hasContent: false } }; + const batches = ruleMigrationsClient.data.resources.searchBatches(migrationId, options); + const missingResources = await batches.all(); // TODO: return only type and name - return res.ok({ body: resources }); + return res.ok({ body: missingResources }); } catch (err) { logger.error(err); return res.badRequest({ body: err.message }); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts index 645fa09b49dc1..9557c5cfd652f 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts @@ -7,6 +7,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources'; import { UpsertRuleMigrationResourcesRequestBody, UpsertRuleMigrationResourcesRequestParams, @@ -49,13 +50,30 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + // Check if the migration exists + const { data } = await ruleMigrationsClient.data.rules.get(migrationId, { size: 1 }); + const [rule] = data; + if (!rule) { + return res.notFound({ body: { message: 'Migration not found' } }); + } + + // Upsert identified resource documents with content const ruleMigrations = resources.map((resource) => ({ - migration_id: migrationId, ...resource, + migration_id: migrationId, })); - await ruleMigrationsClient.data.resources.upsert(ruleMigrations); + // Create identified resource documents without content to keep track of them + const resourceIdentifier = new ResourceIdentifier(rule.original_rule.vendor); + const resourcesToCreate = resourceIdentifier + .fromResources(resources) + .map((resource) => ({ + ...resource, + migration_id: migrationId, + })); + await ruleMigrationsClient.data.resources.create(resourcesToCreate); + return res.ok({ body: { acknowledged: true } }); } catch (err) { logger.error(err); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts index 14825326eee0e..e1fe51a3d3216 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts @@ -5,12 +5,19 @@ * 2.0. */ -import type { SearchHit, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { + SearchHit, + SearchRequest, + SearchResponse, + Duration, +} from '@elastic/elasticsearch/lib/api/types'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import assert from 'assert'; import type { Stored } from '../types'; import type { IndexNameProvider } from './rule_migrations_data_client'; +const DEFAULT_PIT_KEEP_ALIVE: Duration = '30s' as const; + export class RuleMigrationsDataBaseClient { constructor( protected getIndexName: IndexNameProvider, @@ -42,4 +49,48 @@ export class RuleMigrationsDataBaseClient { ? response.hits.total : response.hits.total?.value ?? 0; } + + /** Returns functions to iterate over all the search results in batches */ + protected getSearchBatches( + search: SearchRequest, + keepAlive: Duration = DEFAULT_PIT_KEEP_ALIVE + ) { + let current: Promise> | undefined; + + const pitPromise = this.getIndexName().then((index) => + this.esClient + .openPointInTime({ index, keep_alive: keepAlive }) + .then(({ id }) => ({ id, keep_alive: keepAlive })) + ); + + /* Returns the next batch of search results */ + const next = async (): Promise>> => { + const pit = await pitPromise; + if (!current) { + current = this.esClient.search({ ...search, pit }); + } else { + current = current.then((previousResponse) => { + if (previousResponse.hits.hits.length === 0) { + return previousResponse; + } + const lastSort = previousResponse.hits.hits[previousResponse.hits.hits.length - 1].sort; + return this.esClient.search({ ...search, pit, search_after: lastSort }); + }); + } + return current.then((response) => this.processResponseHits(response)); + }; + + /** Returns all the search results */ + const all = async (): Promise>> => { + const allResults: Array> = []; + let results = await next(); + while (results.length) { + allResults.push(...results); + results = await next(); + } + return allResults; + }; + + return { next, all }; + } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts index 225c682b435b0..8960edd0cce21 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts @@ -6,8 +6,6 @@ */ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import type { RuleMigrationResourceType } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -import { getRuleResourceIdentifier } from '../../../../../common/siem_migrations/rules/resources'; import { RuleMigrationsDataIntegrationsClient } from './rule_migrations_data_integrations_client'; import { RuleMigrationsDataResourcesClient } from './rule_migrations_data_resources_client'; import { RuleMigrationsDataRulesClient } from './rule_migrations_data_rules_client'; @@ -46,54 +44,4 @@ export class RuleMigrationsDataClient { logger ); } - - public async getMissingResources( - migrationId: string - ): Promise> { - const missing: Record> = { - macro: new Set(), - list: new Set(), - }; - - const { data } = await this.rules.get(migrationId); - // This assumes all rules in the migration have the same vendor - const identifyRuleResources = getRuleResourceIdentifier(data[0].original_rule); - - // Identify all resources in the rules - for (const rule of data) { - const identifiedResources = identifyRuleResources(rule.original_rule.query); - for (const type of ['macro', 'list'] as const) { - for (const resource of identifiedResources[type]) { - missing[type].add(resource); - } - } - } - - // Identify all resources in the existing macros - const existingMacroResources = await this.resources.get(migrationId, 'macro'); - for (const macro of existingMacroResources) { - const nestedResourcesIdentified = identifyRuleResources(macro.content); - for (const type of ['macro', 'list'] as const) { - for (const resource of nestedResourcesIdentified[type]) { - missing[type].add(resource); - } - } - } - - // Exclude existing macros - for (const resource of existingMacroResources) { - missing.macro.delete(resource.name); - } - - // Exclude existing lists - const existingListResources = await this.resources.get(migrationId, 'list'); - for (const list of existingListResources) { - missing.list.delete(list.name); - } - - return { - macro: Array.from(missing.macro), - list: Array.from(missing.list), - }; - } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts index 888a41aca944c..cb697de7ffc11 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts @@ -6,7 +6,7 @@ */ import { sha256 } from 'js-sha256'; -import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import type { QueryDslQueryContainer, Duration } from '@elastic/elasticsearch/lib/api/types'; import type { RuleMigrationResource, RuleMigrationResourceType, @@ -14,12 +14,30 @@ import type { import type { StoredRuleMigrationResource } from '../types'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; -export type CreateRuleMigrationResourceInput = Omit; +export type CreateRuleMigrationResourceInput = Pick< + RuleMigrationResource, + 'migration_id' | 'type' | 'name' | 'metadata' +> & { + content?: string; +}; +export interface RuleMigrationResourceFilters { + type?: RuleMigrationResourceType; + names?: string[]; + hasContent?: boolean; +} +export interface RuleMigrationResourceGetOptions { + filters?: RuleMigrationResourceFilters; + size?: number; + from?: number; +} /* BULK_MAX_SIZE defines the number to break down the bulk operations by. * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. */ const BULK_MAX_SIZE = 500 as const; +/* DEFAULT_SEARCH_BATCH_SIZE defines the default number of documents to retrieve per search operation + * when retrieving search results in batches. */ +const DEFAULT_SEARCH_BATCH_SIZE = 500 as const; export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseClient { public async upsert(resources: CreateRuleMigrationResourceInput[]): Promise { @@ -52,24 +70,43 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli } } + /** Creates the resources in the index only if they do not exist */ + public async create(resources: CreateRuleMigrationResourceInput[]): Promise { + const index = await this.getIndexName(); + + let resourcesSlice: CreateRuleMigrationResourceInput[]; + const createdAt = new Date().toISOString(); + while ((resourcesSlice = resources.splice(0, BULK_MAX_SIZE)).length > 0) { + await this.esClient + .bulk({ + refresh: 'wait_for', + operations: resourcesSlice.flatMap((resource) => [ + { create: { _id: this.createId(resource), _index: index } }, + { + ...resource, + '@timestamp': createdAt, + updated_by: this.username, + updated_at: createdAt, + }, + ]), + }) + .catch((error) => { + this.logger.error(`Error upsert resources: ${error.message}`); + throw error; + }); + } + } + public async get( migrationId: string, - type?: RuleMigrationResourceType, - names?: string[] + options: RuleMigrationResourceGetOptions = {} ): Promise { + const { filters, size, from } = options; const index = await this.getIndexName(); - - const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }]; - if (type) { - filter.push({ term: { type } }); - } - if (names) { - filter.push({ terms: { name: names } }); - } - const query = { bool: { filter } }; + const query = this.getFilterQuery(migrationId, filters); return this.esClient - .search({ index, query }) + .search({ index, query, size, from }) .then(this.processResponseHits.bind(this)) .catch((error) => { this.logger.error(`Error searching resources: ${error.message}`); @@ -77,8 +114,46 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli }); } + /** Returns batching functions to traverse all the migration resources search results */ + searchBatches( + migrationId: string, + options: { scroll?: Duration; size?: number; filters?: RuleMigrationResourceFilters } = {} + ) { + const { size = DEFAULT_SEARCH_BATCH_SIZE, filters = {}, scroll } = options; + const query = this.getFilterQuery(migrationId, filters); + const search = { query, sort: '_doc', scroll, size }; // sort by _doc to ensure consistent order + try { + return this.getSearchBatches(search); + } catch (error) { + this.logger.error(`Error scrolling rule migration resources: ${error.message}`); + throw error; + } + } + private createId(resource: CreateRuleMigrationResourceInput): string { const key = `${resource.migration_id}-${resource.type}-${resource.name}`; return sha256.create().update(key).hex(); } + + private getFilterQuery( + migrationId: string, + filters: RuleMigrationResourceFilters = {} + ): QueryDslQueryContainer { + const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }]; + if (filters.type) { + filter.push({ term: { type: filters.type } }); + } + if (filters.names) { + filter.push({ terms: { name: filters.names } }); + } + if (filters.hasContent != null) { + const existContent = { exists: { field: 'content' } }; + if (filters.hasContent) { + filter.push(existContent); + } else { + filter.push({ bool: { must_not: existContent } }); + } + } + return { bool: { filter } }; + } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 716d19ce16cdf..cc8d5fac81657 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -13,6 +13,7 @@ import type { AggregationsStringTermsAggregate, AggregationsStringTermsBucket, QueryDslQueryContainer, + Duration, } from '@elastic/elasticsearch/lib/api/types'; import type { StoredRuleMigration } from '../types'; import { @@ -54,7 +55,9 @@ export interface RuleMigrationGetOptions { * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. */ const BULK_MAX_SIZE = 500 as const; -/* The default number of rule migrations to retrieve in a single GET request. */ +/* DEFAULT_SEARCH_BATCH_SIZE defines the default number of documents to retrieve per search operation + * when retrieving search results in batches. */ +const DEFAULT_SEARCH_BATCH_SIZE = 500 as const; export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { /** Indexes an array of rule migrations to be processed */ @@ -123,7 +126,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { filters = {}, from, size }: RuleMigrationGetOptions = {} ): Promise<{ total: number; data: StoredRuleMigration[] }> { const index = await this.getIndexName(); - const query = this.getFilterQuery(migrationId, { ...filters }); + const query = this.getFilterQuery(migrationId, filters); const result = await this.esClient .search({ index, query, sort: '_doc', from, size }) @@ -137,6 +140,22 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient }; } + /** Returns batching functions to traverse all the migration rules search results */ + searchBatches( + migrationId: string, + options: { scroll?: Duration; size?: number; filters?: RuleMigrationFilters } = {} + ) { + const { size = DEFAULT_SEARCH_BATCH_SIZE, filters = {}, scroll } = options; + const query = this.getFilterQuery(migrationId, filters); + const search = { query, sort: '_doc', scroll, size }; // sort by _doc to ensure consistent order + try { + return this.getSearchBatches(search); + } catch (error) { + this.logger.error(`Error scrolling rule migrations: ${error.message}`); + throw error; + } + } + /** * Retrieves `pending` rule migrations with the provided id and updates their status to `processing`. * This operation is not atomic at migration level: diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.ts index d80646dc27c4d..c6ed55ee48025 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; -import type { QueryResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources/types'; -import { getRuleResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources'; +import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources'; import type { OriginalRule, RuleMigrationResource, @@ -18,83 +16,83 @@ import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_c export type RuleMigrationResources = Partial< Record >; - -/* It's not a common practice to have more than 2-3 nested levels of resources. - * This limit is just to prevent infinite recursion in case something goes wrong. - */ -export const MAX_RECURSION_DEPTH = 30; +interface ExistingResources { + macro: Record; + list: Record; +} export class RuleResourceRetriever { + private existingResources?: ExistingResources; + constructor( private readonly migrationId: string, private readonly dataClient: RuleMigrationsDataClient ) {} - public async getResources(originalRule: OriginalRule): Promise { - const resourceIdentifier = getRuleResourceIdentifier(originalRule); - return this.recursiveRetriever(originalRule.query, resourceIdentifier); - } + public async initialize(): Promise { + const options = { filters: { hasContent: true } }; + const batches = this.dataClient.resources.searchBatches(this.migrationId, options); - private recursiveRetriever = async ( - query: string, - resourceIdentifier: QueryResourceIdentifier, - it = 0 - ): Promise => { - if (it >= MAX_RECURSION_DEPTH) { - return {}; + const existingRuleResources: ExistingResources = { macro: {}, list: {} }; + let resources = await batches.next(); + while (resources.length > 0) { + resources.forEach((resource) => { + existingRuleResources[resource.type][resource.name] = resource; + }); + resources = await batches.next(); } - const identifiedResources = resourceIdentifier(query); - const resources: RuleMigrationResources = {}; - - const listNames = identifiedResources.list; - if (listNames.length > 0) { - const listsWithContent = await this.dataClient.resources - .get(this.migrationId, 'list', listNames) - .then(withContent); + this.existingResources = existingRuleResources; + } - if (listsWithContent.length > 0) { - resources.list = listsWithContent; - } + public async getResources(originalRule: OriginalRule): Promise { + const existingResources = this.existingResources; + if (!existingResources) { + throw new Error('initialize must be called before calling getResources'); } - const macroNames = identifiedResources.macro; - if (macroNames.length > 0) { - const macrosWithContent = await this.dataClient.resources - .get(this.migrationId, 'macro', macroNames) - .then(withContent); + const resourceIdentifier = new ResourceIdentifier(originalRule.vendor); + const resourcesIdentifiedFromRule = resourceIdentifier.fromOriginalRule(originalRule); - if (macrosWithContent.length > 0) { - // retrieve nested resources inside macros - const macrosNestedResources = await Promise.all( - macrosWithContent.map(({ content }) => - this.recursiveRetriever(content, resourceIdentifier, it + 1) - ) - ); - - // Process lists inside macros - const macrosNestedLists = macrosNestedResources.flatMap( - (macroNestedResources) => macroNestedResources.list ?? [] - ); - if (macrosNestedLists.length > 0) { - resources.list = (resources.list ?? []).concat(macrosNestedLists); + const macrosFound = new Map(); + const listsFound = new Map(); + resourcesIdentifiedFromRule.forEach((resource) => { + const existingResource = existingResources[resource.type][resource.name]; + if (existingResource) { + if (resource.type === 'macro') { + macrosFound.set(resource.name, existingResource); + } else if (resource.type === 'list') { + listsFound.set(resource.name, existingResource); } + } + }); + + const resourcesFound = [...macrosFound.values(), ...listsFound.values()]; + if (!resourcesFound.length) { + return {}; + } - // Process macros inside macros - const macrosNestedMacros = macrosNestedResources.flatMap( - (macroNestedResources) => macroNestedResources.macro ?? [] - ); + let nestedResourcesFound = resourcesFound; + do { + const nestedResourcesIdentified = resourceIdentifier.fromResources(nestedResourcesFound); - if (macrosNestedMacros.length > 0) { - macrosWithContent.push(...macrosNestedMacros); + nestedResourcesFound = []; + nestedResourcesIdentified.forEach((resource) => { + const existingResource = existingResources[resource.type][resource.name]; + if (existingResource) { + nestedResourcesFound.push(existingResource); + if (resource.type === 'macro') { + macrosFound.set(resource.name, existingResource); + } else if (resource.type === 'list') { + listsFound.set(resource.name, existingResource); + } } - resources.macro = macrosWithContent; - } - } - return resources; - }; -} + }); + } while (nestedResourcesFound.length > 0); -const withContent = (resources: RuleMigrationResource[]) => { - return resources.filter((resource) => !isEmpty(resource.content)); -}; + return { + ...(macrosFound.size > 0 ? { macro: Array.from(macrosFound.values()) } : {}), + ...(listsFound.size > 0 ? { list: Array.from(listsFound.values()) } : {}), + }; + } +} From 547d5a4d543d22c25a3e46ae8ddb21ec8c0eb860 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 9 Dec 2024 19:40:12 +0100 Subject: [PATCH 28/48] implement macros step --- .../model/api/rules/rule_migration.gen.ts | 2 +- .../api/rules/rule_migration.schema.yaml | 4 +- .../model/rule_migration.gen.ts | 2 +- .../model/rule_migration.schema.yaml | 1 - .../splunk/splunk_identifier.test.ts | 22 +- .../resources/splunk/splunk_identifier.ts | 6 +- .../start_migration/start_migration_card.tsx | 22 +- .../start_migration_check_complete.ts | 5 +- .../start_migration/upload_rules_panels.tsx | 2 +- .../public/siem_migrations/rules/api/index.ts | 42 +++ .../components/data_input_flyout/constants.ts | 15 +- .../data_input_flyout/data_input_flyout.tsx | 77 ++++-- .../steps/common/get_status.ts | 18 ++ .../steps/common/sub_step_wrapper.tsx | 4 +- .../steps/common/use_parse_file_input.ts | 117 ++++++++ .../steps/macros/macros_data_input.tsx | 140 ++++++++++ .../sub_steps/check_resources/index.tsx | 54 ++++ .../sub_steps/check_resources/translations.ts | 20 ++ .../copy_export_query/copy_export_query.tsx | 53 ++++ .../sub_steps/copy_export_query/index.tsx | 26 ++ .../copy_export_query/translations.ts | 18 ++ .../sub_steps/macros_file_upload/index.tsx | 95 +++++++ .../macros_file_upload/macros_file_upload.tsx | 89 ++++++ .../macros_file_upload/translations.ts | 26 ++ .../steps/macros/translations.ts | 13 + .../steps/rules/rules_data_input.tsx | 134 +++++---- .../rules/sub_steps/check_resources/index.tsx | 33 ++- .../copy_export_query/translations.ts | 2 +- .../sub_steps/rules_file_upload/index.tsx | 5 +- .../rules_file_upload/parse_rules_file.ts | 79 ------ .../rules_file_upload/rules_file_upload.tsx | 98 +++---- .../rules_file_upload/translations.ts | 46 +--- .../data_input_flyout/translations.ts | 52 ++++ .../components/data_input_flyout/types.ts | 9 +- .../service/hooks/use_create_migration.ts | 16 +- .../hooks/use_get_missing_resources.ts | 48 ++++ .../rules/service/hooks/use_latest_stats.ts | 8 +- .../service/hooks/use_upsert_resources.ts | 51 ++++ .../rules/service/rule_migrations_service.ts | 26 +- .../rules/api/resources/missing.ts | 9 +- .../rules/data/__mocks__/mocks.ts | 8 + .../rules/task/rule_migrations_task_client.ts | 2 + .../task/util/rule_resource_retriever.test.ts | 254 +++++++----------- .../task/util/rule_resource_retriever.ts | 8 +- 44 files changed, 1309 insertions(+), 452 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/get_status.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/check_resources/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/check_resources/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/copy_export_query.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_missing_resources.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 56c89c0074134..ac8dd3565a705 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -118,7 +118,7 @@ export type GetRuleMigrationResourcesMissingRequestParamsInput = z.input< export type GetRuleMigrationResourcesMissingResponse = z.infer< typeof GetRuleMigrationResourcesMissingResponse >; -export const GetRuleMigrationResourcesMissingResponse = z.array(RuleMigrationResource); +export const GetRuleMigrationResourcesMissingResponse = z.array(RuleMigrationResourceData); export type GetRuleMigrationStatsRequestParams = z.infer; export const GetRuleMigrationStatsRequestParams = z.object({ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index cc1d192c20ccc..10f7ecce6ed73 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -461,7 +461,7 @@ paths: required: true schema: description: The migration id to attach the resources - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates missing migration resources have been retrieved correctly @@ -471,4 +471,4 @@ paths: type: array description: The identified resources missing items: - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationResource' + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationResourceData' diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index b52cdb1c91f19..a2eb67ec475a4 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -308,7 +308,7 @@ export const RuleMigrationResourceData = z.object({ /** * The resource content value. */ - content: z.string(), + content: z.string().optional(), /** * The resource arbitrary metadata. */ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 4c88c66fc604d..1fe991352d81b 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -278,7 +278,6 @@ components: required: - type - name - - content properties: type: $ref: '#/components/schemas/RuleMigrationResourceType' diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.test.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.test.ts index 6fb48353e315c..5d144e5b8a38f 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.test.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.test.ts @@ -21,6 +21,26 @@ describe('splResourceIdentifier', () => { ]); }); + it('should extract macros with double quotes parameters correctly', () => { + const query = '| `macro_one("90","2")` | `macro_two("20")`'; + + const result = splResourceIdentifier(query); + expect(result).toEqual([ + { type: 'macro', name: 'macro_one(2)' }, + { type: 'macro', name: 'macro_two(1)' }, + ]); + }); + + it('should extract macros with single quotes parameters correctly', () => { + const query = "| `macro_one('90','2')` | `macro_two('20')`"; + + const result = splResourceIdentifier(query); + expect(result).toEqual([ + { type: 'macro', name: 'macro_one(2)' }, + { type: 'macro', name: 'macro_two(1)' }, + ]); + }); + it('should extract lookup tables correctly', () => { const query = 'search ... | lookup my_lookup_table field AS alias OUTPUT new_field | lookup other_lookup_list | lookup third_lookup'; @@ -104,7 +124,7 @@ describe('splResourceIdentifier', () => { it('should ignore macros or lookup tables inside comments wrapped by ```', () => { const query = - '`macro_one` | ```this is a comment with `macro_two` and lookup another_table``` lookup my_lookup_table'; + '`macro_one` ```this is a comment with `macro_two` and lookup another_table``` | lookup my_lookup_table ```this is another comment```'; const result = splResourceIdentifier(query); expect(result).toEqual([ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.ts b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.ts index 0fafbd7caf406..2ecc43321b11f 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/rules/resources/splunk/splunk_identifier.ts @@ -40,10 +40,10 @@ export const splResourceIdentifier: ResourceIdentifier = (input) => { }; // Comments should be removed before processing the query to avoid matching macro and list names inside them -const commentRegex = /```.*```/g; +const commentRegex = /```.*?```/g; // Literal strings should be replaced with a placeholder to avoid matching macro and list names inside them -const doubleQuoteStrRegex = /".*"/g; -const singleQuoteStrRegex = /'.*'/g; +const doubleQuoteStrRegex = /".*?"/g; +const singleQuoteStrRegex = /'.*?'/g; // lookup operator can have modifiers like local=true or update=false before the lookup name, we need to remove them const lookupModifiers = /\blookup\b\s+((local|update)=\s*(?:true|false)\s*)+/gi; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index c1e7539c8e101..a8d7aa78d0c93 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiSpacer, EuiText } from '@elastic/eui'; +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'; @@ -21,22 +22,29 @@ import * as i18n from './translations'; import { MissingAIConnectorCallout } from './missing_ai_connector_callout'; export const StartMigrationCard: OnboardingCardComponent = React.memo( - ({ checkComplete, isCardComplete, setExpandedCardId }) => { + ({ setComplete, isCardComplete, setExpandedCardId }) => { const styles = useStyles(); - const { data: migrationsStats, isLoading } = useLatestStats(); + 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) { + if (migrationsStats.some(({ status }) => status === SiemMigrationTaskStatus.FINISHED)) { + setComplete(true); + } + } + }, [isCardComplete, migrationsStats, setComplete]); + const closeFlyout = useCallback(() => { setIsFlyoutOpen(false); setFlyoutMigrationStats(undefined); - if (!isCardComplete(OnboardingCardId.siemMigrationsStart)) { - checkComplete(); - } - }, [checkComplete, isCardComplete]); + refreshStats(); + }, [refreshStats]); const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => { setFlyoutMigrationStats(migrationStats); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts index 41e65352d4bc3..79a7238b9554e 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts @@ -5,12 +5,15 @@ * 2.0. */ +import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; import type { OnboardingCardCheckComplete } from '../../../../../types'; export const checkStartMigrationCardComplete: OnboardingCardCheckComplete = async ({ siemMigrations, }) => { const migrationsStats = await siemMigrations.rules.getRuleMigrationsStats(); - const isComplete = migrationsStats.length > 0; + const isComplete = migrationsStats.some( + (migrationStats) => migrationStats.status === SiemMigrationTaskStatus.FINISHED + ); return isComplete; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx index 95b53d921fd1f..6d011fc5fbb5b 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx @@ -28,7 +28,7 @@ export const UploadRulesPanels = React.memo(({ migration
{migrationsStats.map((migrationStats) => ( - + {migrationStats.status === SiemMigrationTaskStatus.READY && ( )} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts index db6f0117d4a77..eb5495929a85d 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -19,6 +19,8 @@ import { SIEM_RULE_MIGRATION_START_PATH, SIEM_RULE_MIGRATION_STATS_PATH, SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, + SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH, + SIEM_RULE_MIGRATION_RESOURCES_PATH, } from '../../../../common/siem_migrations/constants'; import type { CreateRuleMigrationRequestBody, @@ -30,6 +32,9 @@ import type { InstallMigrationRulesResponse, StartRuleMigrationRequestBody, GetRuleMigrationStatsResponse, + GetRuleMigrationResourcesMissingResponse, + UpsertRuleMigrationResourcesRequestBody, + UpsertRuleMigrationResourcesResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; export interface GetRuleMigrationStatsParams { @@ -84,6 +89,43 @@ export const createRuleMigration = async ({ ); }; +export interface GetRuleMigrationMissingResourcesParams { + /** `id` of the migration to start */ + migrationId: string; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Starts a new migration with the provided rules. */ +export const getMissingResources = async ({ + migrationId, + signal, +}: GetRuleMigrationMissingResourcesParams): Promise => { + return KibanaServices.get().http.get( + replaceParams(SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH, { migration_id: migrationId }), + { version: '1', signal } + ); +}; + +export interface UpsertResourcesParams { + /** Optional `id` of migration to add the rules to. */ + migrationId: string; + /** The body containing the `connectorId` to use for the migration */ + body: UpsertRuleMigrationResourcesRequestBody; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Starts a new migration with the provided rules. */ +export const upsertMigrationResources = async ({ + migrationId, + body, + signal, +}: UpsertResourcesParams): Promise => { + return KibanaServices.get().http.post( + replaceParams(SIEM_RULE_MIGRATION_RESOURCES_PATH, { migration_id: migrationId }), + { body: JSON.stringify(body), version: '1', signal } + ); +}; + export interface StartRuleMigrationParams { /** `id` of the migration to start */ migrationId: string; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts index aa331bf17c832..d390b395ed98f 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts @@ -5,13 +5,7 @@ * 2.0. */ -export enum DataInputStep { - rules = 'rules', - macros = 'macros', - lookups = 'lookups', -} - -export const SPL_RULES_COLUMNS = [ +export const SPLUNK_RULES_COLUMNS = [ 'id', 'title', 'search', @@ -23,4 +17,9 @@ export const SPL_RULES_COLUMNS = [ export const RULES_SPLUNK_QUERY = `| rest /servicesNS/-/-/saved/searches | search action.correlationsearch.enabled = "1" OR (eai:acl.app = "Splunk_Security_Essentials" AND is_scheduled=1) | where disabled=0 -| table ${SPL_RULES_COLUMNS.join(', ')}`; +| table ${SPLUNK_RULES_COLUMNS.join(', ')}`; + +export const SPLUNK_MACROS_COLUMNS = ['title', 'definition'] as const; + +export const MACROS_SPLUNK_QUERY = `| rest /servicesNS/-/-/admin/macros count=0 +| table ${SPLUNK_MACROS_COLUMNS.join(', ')}`; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index 6a4916a5e54b3..92479ce516977 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -17,10 +17,19 @@ import { EuiButton, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -import { DataInputStep } from './constants'; +import type { + RuleMigrationResourceData, + RuleMigrationTaskStats, +} 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 type { DataInputStep } from './types'; +import { MacrosDataInput } from './steps/macros/macros_data_input'; + +interface MissingResourcesIndexed { + macros: string[]; + lookups: string[]; +} export interface MigrationDataInputFlyoutProps { onClose: () => void; @@ -31,6 +40,9 @@ export const MigrationDataInputFlyout = React.memo( initialMigrationSats ); + const [missingResourcesIndexed, setMissingResourcesIndexed] = useState< + MissingResourcesIndexed | undefined + >(); const { startMigration, isLoading: isStartLoading } = useStartMigration(onClose); const onStartMigration = useCallback(() => { @@ -39,23 +51,35 @@ export const MigrationDataInputFlyout = React.memo(() => { - if (migrationStats) { - return DataInputStep.macros; - } - return DataInputStep.rules; - }); + const [dataInputStep, setDataInputStep] = useState(1); - const onMigrationCreated = useCallback( - (createdMigrationStats: RuleMigrationTaskStats) => { - if (createdMigrationStats) { - setMigrationStats(createdMigrationStats); - setDataInputStep(DataInputStep.macros); - } + const onMigrationCreated = useCallback((createdMigrationStats: RuleMigrationTaskStats) => { + setMigrationStats(createdMigrationStats); + }, []); + + const onMissingResourcesFetched = useCallback( + (missingResources: RuleMigrationResourceData[]) => { + const newMissingResourcesIndexed = missingResources.reduce( + (acc, { type, name }) => { + if (type === 'macro') { + acc.macros.push(name); + } else if (type === 'list') { + acc.lookups.push(name); + } + return acc; + }, + { macros: [], lookups: [] } + ); + setMissingResourcesIndexed(newMissingResourcesIndexed); + setDataInputStep(2); }, - [setDataInputStep] + [] ); + const onMacrosCreated = useCallback(() => { + setDataInputStep(3); + }, []); + return ( - + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/get_status.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/get_status.ts new file mode 100644 index 0000000000000..f9f14298d00be --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/get_status.ts @@ -0,0 +1,18 @@ +/* + * 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 { EuiStepStatus } from '@elastic/eui'; + +export const getStatus = (step: number, currentStep: number): EuiStepStatus => { + if (step === currentStep) { + return 'current'; + } + if (step < currentStep) { + return 'complete'; + } + return 'incomplete'; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx index 438134b0ad99a..fc0bd0e8c3b44 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx @@ -16,11 +16,11 @@ const style = css` } `; -export const SubStepWrapper = React.memo>(({ children }) => { +export const SubStepsWrapper = React.memo>(({ children }) => { return ( {children} ); }); -SubStepWrapper.displayName = 'SubStepWrapper'; +SubStepsWrapper.displayName = 'SubStepsWrapper'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts new file mode 100644 index 0000000000000..b99cf826194f9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts @@ -0,0 +1,117 @@ +/* + * 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 { useState, useCallback } from 'react'; +import { FILE_UPLOAD_ERROR } from '../../translations'; + +export interface SplunkRow { + result: T; +} + +export type OnFileParsed = (content: SplunkRow[]) => void; + +export const useParseFileInput = (onFileParsed: OnFileParsed) => { + const [isParsing, setIsParsing] = useState(false); + const [error, setError] = useState(); + + const parseFile = useCallback( + (files: FileList | null) => { + if (!files) { + return; + } + + setError(undefined); + + const rulesFile = files[0]; + 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 fileContent = e.target?.result as string | undefined; + + if (fileContent == null) { + setError(FILE_UPLOAD_ERROR.CAN_NOT_READ); + return; + } + + if (fileContent === '' && 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 + setError(FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + return; + } + + try { + const parsedData = parseContent(fileContent); + onFileParsed(parsedData); + } catch (err) { + setError(err.message); + } + }; + + const handleReaderError = function () { + const message = reader.error?.message; + if (message) { + setError(FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); + } else { + setError(FILE_UPLOAD_ERROR.CAN_NOT_READ); + } + }; + + reader.onerror = handleReaderError; + reader.onabort = handleReaderError; + + reader.readAsText(rulesFile); + }, + [onFileParsed] + ); + + return { parseFile, isParsing, error }; +}; + +const parseContent = (fileContent: string): SplunkRow[] => { + const trimmedContent = fileContent.trim(); + let arrayContent: SplunkRow[]; + if (trimmedContent.startsWith('[')) { + arrayContent = parseJSONArray(trimmedContent); + } else { + arrayContent = parseNDJSON(trimmedContent); + } + if (arrayContent.length === 0) { + throw new Error(FILE_UPLOAD_ERROR.EMPTY); + } + return arrayContent; +}; + +const parseNDJSON = (fileContent: string): SplunkRow[] => { + return fileContent + .split(/\n(?=\{)/) // split at newline followed by '{'. + .filter((entry) => entry.trim() !== '') // Remove empty entries. + .map(parseJSON); // Parse each entry as JSON. +}; + +const parseJSONArray = (fileContent: string): SplunkRow[] => { + const parsedContent = parseJSON(fileContent); + if (!Array.isArray(parsedContent)) { + throw new Error(FILE_UPLOAD_ERROR.NOT_ARRAY); + } + return parsedContent; +}; + +const parseJSON = (fileContent: string) => { + try { + return JSON.parse(fileContent); + } catch (error) { + if (error instanceof RangeError) { + throw new Error(FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + } + throw new Error(FILE_UPLOAD_ERROR.CAN_NOT_PARSE); + } +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx new file mode 100644 index 0000000000000..f19e704b96710 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx @@ -0,0 +1,140 @@ +/* + * 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, + EuiSteps, + 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 { getStatus } from '../common/get_status'; +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 + extends Omit { + dataInputStep: DataInputStep; + migrationStats?: RuleMigrationTaskStats; + missingMacros?: string[]; +} +export const MacrosDataInput = React.memo( + ({ + dataInputStep, + migrationStats, + missingMacros, + onMacrosCreated, + onMissingResourcesFetched, + }) => { + const dataInputStatus = useMemo( + () => getStatus(DataInputStepNumber, dataInputStep), + [dataInputStep] + ); + + return ( + + + + + + + + + + {i18n.MACROS_DATA_INPUT_TITLE} + + + + + {dataInputStatus === 'current' && migrationStats && missingMacros && ( + + + + )} + + + ); + } +); +MacrosDataInput.displayName = 'MacrosDataInput'; + +const END = 10 as const; +type SubStep = 1 | 2 | 3 | typeof END; +export const MacrosDataInputSubSteps = React.memo( + ({ migrationStats, missingMacros, onMacrosCreated, onMissingResourcesFetched }) => { + const [subStep, setSubStep] = useState(missingMacros.length ? 1 : 3); + + // Copy query step + const onCopied = useCallback(() => { + setSubStep(2); + }, []); + const copyStep = useCopyExportQueryStep({ status: getStatus(1, subStep), onCopied }); + + // Upload macros step + const onMacrosCreatedStep = useCallback(() => { + onMacrosCreated(); + setSubStep(3); + }, [onMacrosCreated]); + const uploadStep = useMacrosFileUploadStep({ + status: getStatus(2, subStep), + migrationStats, + missingMacros, + onMacrosCreated: onMacrosCreatedStep, + }); + + // Check missing resources step + const onMissingResourcesFetchedStep = useCallback( + (newMissingResources) => { + onMissingResourcesFetched(newMissingResources); + setSubStep(END); + }, + [onMissingResourcesFetched] + ); + const resourcesStep = useCheckResourcesStep({ + status: getStatus(3, subStep), + migrationStats, + onMissingResourcesFetched: onMissingResourcesFetchedStep, + }); + + const steps = useMemo( + () => [copyStep, uploadStep, resourcesStep], + [copyStep, uploadStep, resourcesStep] + ); + + return ( + + + + ); + } +); +MacrosDataInputSubSteps.displayName = 'MacrosDataInputActive'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/check_resources/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/check_resources/index.tsx new file mode 100644 index 0000000000000..d83890e1f260c --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/check_resources/index.tsx @@ -0,0 +1,54 @@ +/* + * 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, { useEffect, useMemo } from 'react'; +import { EuiText, type EuiStepProps, type EuiStepStatus } from '@elastic/eui'; +// import { useGetMissingResources } from '../../../../../../logic/use_get_migration_missing_resources'; +import type { RuleMigrationTaskStats } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { useGetMissingResources } from '../../../../../../service/hooks/use_get_missing_resources'; +import type { OnMissingResourcesFetched } from '../../../../types'; +import * as i18n from './translations'; + +export interface CheckResourcesStepProps { + status: EuiStepStatus; + migrationStats: RuleMigrationTaskStats | undefined; + onMissingResourcesFetched: OnMissingResourcesFetched; +} +export const useCheckResourcesStep = ({ + status, + migrationStats, + onMissingResourcesFetched, +}: CheckResourcesStepProps): EuiStepProps => { + const { getMissingResources, isLoading, error } = + useGetMissingResources(onMissingResourcesFetched); + + useEffect(() => { + if (status === 'current' && migrationStats?.id) { + getMissingResources(migrationStats.id); + } + }, [getMissingResources, status, migrationStats?.id]); + + const uploadStepStatus = useMemo(() => { + if (isLoading) { + return 'loading'; + } + if (error) { + return 'danger'; + } + return status; + }, [isLoading, error, status]); + + return { + title: i18n.RULES_DATA_INPUT_CHECK_RESOURCES_TITLE, + status: uploadStepStatus, + children: ( + + {i18n.RULES_DATA_INPUT_CHECK_RESOURCES_DESCRIPTION} + + ), + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/check_resources/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/check_resources/translations.ts new file mode 100644 index 0000000000000..159b4033fafd6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/check_resources/translations.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULES_DATA_INPUT_CHECK_RESOURCES_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.checkResources.title', + { defaultMessage: 'Check for macros and lookups' } +); + +export const RULES_DATA_INPUT_CHECK_RESOURCES_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.checkResources.description', + { + defaultMessage: `For best translation results, we will automatically review your rules for macros and lookups and ask you to upload them. Once uploaded, we'll be able to deliver a more complete rule translation for all rules using those macros or lookups.`, + } +); diff --git a/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/copy_export_query.tsx new file mode 100644 index 0000000000000..93f2ce715184c --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/copy_export_query.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { MACROS_SPLUNK_QUERY } from '../../../../constants'; +import * as i18n from './translations'; + +interface CopyExportQueryProps { + onCopied: () => void; +} +export const CopyExportQuery = React.memo(({ onCopied }) => { + const onClick: React.MouseEventHandler = useCallback( + (ev) => { + // The only button inside the element is the "copy" button. + if ((ev.target as Element).tagName === 'BUTTON') { + onCopied(); + } + }, + [onCopied] + ); + + return ( + <> + {/* The click event is also dispatched when using the keyboard actions (space or enter) for "copy" button. + No need to use keyboard specific events, disabling the a11y lint rule:*/} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
+ {/* onCopy react event is dispatched when the user copies text manually */} + + {MACROS_SPLUNK_QUERY} + +
+ + + {i18n.RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION}, + format: {'JSON'}, + }} + /> + + + ); +}); +CopyExportQuery.displayName = 'CopyExportQuery'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx new file mode 100644 index 0000000000000..3d2adcc78857b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { CopyExportQuery } from './copy_export_query'; +import * as i18n from './translations'; + +export interface CopyExportQueryStepProps { + status: EuiStepStatus; + onCopied: () => void; +} +export const useCopyExportQueryStep = ({ + status, + onCopied, +}: CopyExportQueryStepProps): EuiStepProps => { + return { + title: i18n.RULES_DATA_INPUT_COPY_TITLE, + status, + children: , + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts new file mode 100644 index 0000000000000..71466a54dd138 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts @@ -0,0 +1,18 @@ +/* + * 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 RULES_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( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.copyExportQuery.description.section', + { defaultMessage: 'Search and Reporting' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx new file mode 100644 index 0000000000000..bca72e77487fb --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { ResourceIdentifier } from '../../../../../../../../../common/siem_migrations/rules/resources'; +import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources'; +import type { + RuleMigrationResourceData, + RuleMigrationTaskStats, +} from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { OnResourcesCreated } from '../../../../types'; +import { MacrosFileUpload } from './macros_file_upload'; +import * as i18n from './translations'; + +export interface RulesFileUploadStepProps { + status: EuiStepStatus; + migrationStats: RuleMigrationTaskStats; + missingMacros: string[]; + onMacrosCreated: OnResourcesCreated; +} +export const useMacrosFileUploadStep = ({ + status, + migrationStats, + missingMacros, + onMacrosCreated, +}: RulesFileUploadStepProps): EuiStepProps => { + const { upsertResources, isLoading, error } = useUpsertResources(onMacrosCreated); + + const upsertMigrationResources = useCallback( + (macrosFromFile: RuleMigrationResourceData[]) => { + const macrosIndexed: Record = Object.fromEntries( + macrosFromFile.map((macro) => [macro.name, macro]) + ); + const resourceIdentifier = new ResourceIdentifier('splunk'); + const macrosToUpsert: RuleMigrationResourceData[] = []; + let missingMacrosIt: string[] = missingMacros; + + while (missingMacrosIt.length > 0) { + const macros: RuleMigrationResourceData[] = []; + missingMacrosIt.forEach((macroName) => { + const macro = macrosIndexed[macroName]; + if (macro) { + macros.push(macro); + } else { + // Macro missing from file + } + }); + macrosToUpsert.push(...macros); + + missingMacrosIt = resourceIdentifier + .fromResources(macros) + .reduce((acc, resource) => { + if (resource.type === 'macro') { + acc.push(resource.name); + } + return acc; + }, []); + } + + if (macrosToUpsert.length === 0) { + return; // No missing macros provided + } + upsertResources(migrationStats.id, macrosToUpsert); + }, + [upsertResources, migrationStats, missingMacros] + ); + + const uploadStepStatus = useMemo(() => { + if (isLoading) { + return 'loading'; + } + if (error) { + return 'danger'; + } + return status; + }, [isLoading, error, status]); + + return { + title: i18n.RULES_DATA_INPUT_FILE_UPLOAD_TITLE, + status: uploadStepStatus, + children: ( + + ), + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx new file mode 100644 index 0000000000000..06983ccd30f6e --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.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, useMemo } from 'react'; +import { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; +import { isPlainObject } from 'lodash'; +import type { RuleMigrationResourceData } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { FILE_UPLOAD_ERROR } from '../../../../translations'; +import type { SPLUNK_MACROS_COLUMNS } from '../../../../constants'; +import { useParseFileInput, type SplunkRow } from '../../../common/use_parse_file_input'; +import * as i18n from './translations'; + +type SplunkMacroResult = Partial>; + +export interface MacrosFileUploadProps { + createResources: (resources: RuleMigrationResourceData[]) => void; + apiError?: string; + isLoading?: boolean; +} +export const MacrosFileUpload = React.memo( + ({ createResources, apiError, isLoading }) => { + const onFileParsed = useCallback( + (content: Array>) => { + const rules = content.map(formatMacroRow); + createResources(rules); + }, + [createResources] + ); + + const { parseFile, isParsing, error: fileError } = useParseFileInput(onFileParsed); + + const error = useMemo(() => { + if (apiError) { + return apiError; + } + return fileError; + }, [apiError, fileError]); + + return ( + + {error} + + } + isInvalid={error != null} + fullWidth + > + + + {i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT} + + + } + accept="application/json" + onChange={parseFile} + display="large" + aria-label="Upload logs sample file" + isLoading={isParsing || isLoading} + disabled={isParsing || isLoading} + data-test-subj="macrosFilePicker" + data-loading={isParsing} + /> + + ); + } +); +MacrosFileUpload.displayName = 'MacrosFileUpload'; + +const formatMacroRow = (row: SplunkRow): RuleMigrationResourceData => { + if (!isPlainObject(row.result)) { + throw new Error(FILE_UPLOAD_ERROR.NOT_OBJECT); + } + const macroResource: Partial = { + type: 'macro', + name: row.result.title, + content: row.result.definition, + }; + // resource document format validation delegated to API + return macroResource as RuleMigrationResourceData; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts new file mode 100644 index 0000000000000..25b64787d6dcd --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts @@ -0,0 +1,26 @@ +/* + * 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 RULES_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( + '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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/translations.ts new file mode 100644 index 0000000000000..7061e91311308 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const MACROS_DATA_INPUT_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.title', + { defaultMessage: 'Upload identified macros' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx index 2b20dcda0cea7..acc22a030b02f 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import type { EuiStepProps } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, @@ -14,57 +14,31 @@ import { EuiSteps, EuiTitle, } from '@elastic/eui'; -import React, { useMemo, useState } from 'react'; -import { SubStepWrapper } from '../common/sub_step_wrapper'; -import type { OnMigrationCreated } from '../../types'; +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 { 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'; -type Step = 1 | 2 | 3 | 4; -const getStatus = (step: Step, currentStep: Step): EuiStepStatus => { - if (step === currentStep) { - return 'current'; - } - if (step < currentStep) { - return 'complete'; - } - return 'incomplete'; -}; +const DataInputStepNumber: DataInputStep = 1; -interface RulesDataInputProps { - selected: boolean; +interface RulesDataInputSubStepsProps { + migrationStats?: RuleMigrationTaskStats; onMigrationCreated: OnMigrationCreated; + onMissingResourcesFetched: OnMissingResourcesFetched; +} +interface RulesDataInputProps extends RulesDataInputSubStepsProps { + dataInputStep: DataInputStep; } - export const RulesDataInput = React.memo( - ({ selected, onMigrationCreated }) => { - const [step, setStep] = useState(1); - - const copyStep = useCopyExportQueryStep({ - status: getStatus(1, step), - onCopied: () => setStep(2), - }); - - const uploadStep = useRulesFileUploadStep({ - status: getStatus(2, step), - onMigrationCreated: (stats) => { - onMigrationCreated(stats); - setStep(3); - }, - }); - - const resourcesStep = useCheckResourcesStep({ - status: getStatus(3, step), - onComplete: () => { - setStep(4); - }, - }); - - const steps = useMemo( - () => [copyStep, uploadStep, resourcesStep], - [copyStep, uploadStep, resourcesStep] + ({ dataInputStep, migrationStats, onMigrationCreated, onMissingResourcesFetched }) => { + const dataInputStatus = useMemo( + () => getStatus(DataInputStepNumber, dataInputStep), + [dataInputStep] ); return ( @@ -73,7 +47,11 @@ export const RulesDataInput = React.memo( - + @@ -82,14 +60,72 @@ export const RulesDataInput = React.memo( - - - - - + {dataInputStatus === 'current' && ( + + + + )}
); } ); RulesDataInput.displayName = 'RulesDataInput'; + +const END = 10 as const; +type SubStep = 1 | 2 | 3 | typeof END; +export const RulesDataInputSubSteps = React.memo( + ({ migrationStats, onMigrationCreated, onMissingResourcesFetched }) => { + const [subStep, setSubStep] = useState(migrationStats ? 3 : 1); + + // Copy query step + const onCopied = useCallback(() => { + setSubStep(2); + }, []); + const copyStep = useCopyExportQueryStep({ status: getStatus(1, subStep), onCopied }); + + // Upload rules step + const onMigrationCreatedStep = useCallback( + (stats) => { + onMigrationCreated(stats); + setSubStep(3); + }, + [onMigrationCreated] + ); + const uploadStep = useRulesFileUploadStep({ + status: getStatus(2, subStep), + migrationStats, + onMigrationCreated: onMigrationCreatedStep, + }); + + // Check missing resources step + const onMissingResourcesFetchedStep = useCallback( + (missingResources) => { + onMissingResourcesFetched(missingResources); + setSubStep(END); + }, + [onMissingResourcesFetched] + ); + const resourcesStep = useCheckResourcesStep({ + status: getStatus(3, subStep), + migrationStats, + onMissingResourcesFetched: onMissingResourcesFetchedStep, + }); + + const steps = useMemo( + () => [copyStep, uploadStep, resourcesStep], + [copyStep, uploadStep, resourcesStep] + ); + + return ( + + + + ); + } +); +RulesDataInputSubSteps.displayName = 'RulesDataInputActive'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx index 3b081eb203267..02aa109872f4a 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx @@ -5,22 +5,45 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useMemo } from 'react'; import { EuiText, type EuiStepProps, type EuiStepStatus } from '@elastic/eui'; +import type { RuleMigrationTaskStats } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { useGetMissingResources } from '../../../../../../service/hooks/use_get_missing_resources'; +import type { OnMissingResourcesFetched } from '../../../../types'; import * as i18n from './translations'; export interface CheckResourcesStepProps { status: EuiStepStatus; - onComplete: () => void; + migrationStats: RuleMigrationTaskStats | undefined; + onMissingResourcesFetched: OnMissingResourcesFetched; } export const useCheckResourcesStep = ({ status, - onComplete, + migrationStats, + onMissingResourcesFetched, }: CheckResourcesStepProps): EuiStepProps => { - // onComplete(); // TODO: check the resources + const { getMissingResources, isLoading, error } = + useGetMissingResources(onMissingResourcesFetched); + + useEffect(() => { + if (status === 'current' && migrationStats?.id) { + getMissingResources(migrationStats.id); + } + }, [getMissingResources, status, migrationStats?.id]); + + const uploadStepStatus = useMemo(() => { + if (isLoading) { + return 'loading'; + } + if (error) { + return 'danger'; + } + return status; + }, [isLoading, error, status]); + return { title: i18n.RULES_DATA_INPUT_CHECK_RESOURCES_TITLE, - status, + status: uploadStepStatus, children: ( {i18n.RULES_DATA_INPUT_CHECK_RESOURCES_DESCRIPTION} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts index d76eb71f2e378..78a0636661604 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const RULES_DATA_INPUT_COPY_TITLE = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.title', - { defaultMessage: 'Copy and export query' } + { defaultMessage: 'Copy rules query' } ); export const RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx index ab7838b28908b..97dfd903e499e 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import type { RuleMigrationTaskStats } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { OnMigrationCreated } from '../../../../types'; import { RulesFileUpload } from './rules_file_upload'; import { @@ -17,13 +18,15 @@ import * as i18n from './translations'; export interface RulesFileUploadStepProps { status: EuiStepStatus; + migrationStats?: RuleMigrationTaskStats; onMigrationCreated: OnMigrationCreated; } export const useRulesFileUploadStep = ({ status, + migrationStats, onMigrationCreated, }: RulesFileUploadStepProps): EuiStepProps => { - const [isCreated, setIsCreated] = useState(false); + const [isCreated, setIsCreated] = useState(!!migrationStats); const onSuccess = useCallback( (stats) => { setIsCreated(true); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts deleted file mode 100644 index 3d5dbb32ccde8..0000000000000 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts +++ /dev/null @@ -1,79 +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 { isPlainObject } from 'lodash/fp'; -import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; -import type { SPL_RULES_COLUMNS } from '../../../../constants'; -import * as i18n from './translations'; - -type SplunkResult = Partial>; -interface SplunkRow { - result: SplunkResult; -} - -export const parseContent = (fileContent: string): OriginalRule[] => { - const trimmedContent = fileContent.trim(); - let arrayContent: SplunkRow[]; - if (trimmedContent.startsWith('[')) { - arrayContent = parseJSONArray(trimmedContent); - } else { - arrayContent = parseNDJSON(trimmedContent); - } - if (arrayContent.length === 0) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.EMPTY); - } - return arrayContent.map(convertFormat); -}; - -const parseNDJSON = (fileContent: string): SplunkRow[] => { - return fileContent - .split(/\n(?=\{)/) // split at newline followed by '{'. - .filter((entry) => entry.trim() !== '') // Remove empty entries. - .map(parseJSON); // Parse each entry as JSON. -}; - -const parseJSONArray = (fileContent: string): SplunkRow[] => { - const parsedContent = parseJSON(fileContent); - if (!Array.isArray(parsedContent)) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_ARRAY); - } - return parsedContent; -}; - -const parseJSON = (fileContent: string) => { - try { - return JSON.parse(fileContent); - } catch (error) { - if (error instanceof RangeError) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); - } - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_PARSE); - } -}; - -const convertFormat = (row: SplunkRow): OriginalRule => { - if (!isPlainObject(row) || !isPlainObject(row.result)) { - throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_OBJECT); - } - const originalRule: Partial = { - id: row.result.id, - vendor: 'splunk', - title: row.result.title, - query: row.result.search, - query_language: 'spl', - description: row.result['action.escu.eli5']?.trim() || row.result.description, - }; - - if (row.result['action.correlationsearch.annotations']) { - try { - originalRule.annotations = JSON.parse(row.result['action.correlationsearch.annotations']); - } catch (error) { - delete originalRule.annotations; - } - } - return originalRule as OriginalRule; -}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx index 0f9787a4ddf68..d378028df507e 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx @@ -5,12 +5,17 @@ * 2.0. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; +import { isPlainObject } from 'lodash'; import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { CreateMigration } from '../../../../../../service/hooks/use_create_migration'; -import { parseContent } from './parse_rules_file'; import * as i18n from './translations'; +import { FILE_UPLOAD_ERROR } from '../../../../translations'; +import { useParseFileInput, type SplunkRow } from '../../../common/use_parse_file_input'; +import type { SPLUNK_RULES_COLUMNS } from '../../../../constants'; + +type SplunkRulesResult = Partial>; export interface RulesFileUploadProps { createMigration: CreateMigration; @@ -20,65 +25,16 @@ export interface RulesFileUploadProps { } export const RulesFileUpload = React.memo( ({ createMigration, apiError, isLoading, isCreated }) => { - const [isParsing, setIsParsing] = useState(false); - const [fileError, setFileError] = useState(); - - const onChangeFile = useCallback( - (files: FileList | null) => { - if (!files) { - return; - } - - setFileError(undefined); - - const rulesFile = files[0]; - 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 fileContent = e.target?.result as string | undefined; - - if (fileContent == null) { - setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); - return; - } - - if (fileContent === '' && 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 - setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); - return; - } - - let data: OriginalRule[]; - try { - data = parseContent(fileContent); - createMigration(data); - } catch (err) { - setFileError(err.message); - } - }; - - const handleReaderError = function () { - const message = reader.error?.message; - if (message) { - setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); - } else { - setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); - } - }; - - reader.onerror = handleReaderError; - reader.onabort = handleReaderError; - - reader.readAsText(rulesFile); + const onFileParsed = useCallback( + (content: Array>) => { + const rules = content.map(formatRuleRow); + createMigration(rules); }, [createMigration] ); + const { parseFile, isParsing, error: fileError } = useParseFileInput(onFileParsed); + const error = useMemo(() => { if (apiError) { return apiError; @@ -107,9 +63,9 @@ export const RulesFileUpload = React.memo( } accept="application/json" - onChange={onChangeFile} + onChange={parseFile} display="large" - aria-label="Upload logs sample file" + aria-label="Upload rules file" isLoading={isParsing || isLoading} disabled={isLoading || isCreated} data-test-subj="rulesFilePicker" @@ -120,3 +76,27 @@ export const RulesFileUpload = React.memo( } ); RulesFileUpload.displayName = 'RulesFileUpload'; + +const formatRuleRow = (row: SplunkRow): OriginalRule => { + if (!isPlainObject(row.result)) { + throw new Error(FILE_UPLOAD_ERROR.NOT_OBJECT); + } + const originalRule: Partial = { + id: row.result.id, + vendor: 'splunk', + title: row.result.title, + query: row.result.search, + query_language: 'spl', + description: row.result['action.escu.eli5']?.trim() || row.result.description, + }; + + if (row.result['action.correlationsearch.annotations']) { + try { + originalRule.annotations = JSON.parse(row.result['action.correlationsearch.annotations']); + } catch (error) { + delete originalRule.annotations; + } + } + // rule document format validation delegated to API + return originalRule as OriginalRule; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts index 675eed61f4973..b560849ca1cd7 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts @@ -9,57 +9,13 @@ import { i18n } from '@kbn/i18n'; export const RULES_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.title', - { defaultMessage: 'Update your rule export' } + { defaultMessage: 'Update your rules export' } ); export const RULES_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.prompt', { defaultMessage: 'Select or drag and drop the exported JSON file' } ); -export const RULES_DATA_INPUT_FILE_UPLOAD_ERROR = { - CAN_NOT_READ: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotRead', - { defaultMessage: 'Failed to read the rules export file' } - ), - CAN_NOT_READ_WITH_REASON: (reason: string) => - i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotReadWithReason', - { - defaultMessage: 'An error occurred when reading rules export file: {reason}', - values: { reason }, - } - ), - CAN_NOT_PARSE: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotParse', - { defaultMessage: 'Cannot parse the rules export file as either a JSON file' } - ), - TOO_LARGE_TO_PARSE: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.tooLargeToParse', - { defaultMessage: 'This rules export file is too large to parse' } - ), - NOT_ARRAY: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notArray', - { defaultMessage: 'The rules export file is not an array' } - ), - EMPTY: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.empty', - { defaultMessage: 'The rules export file is empty' } - ), - NOT_OBJECT: i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notObject', - { defaultMessage: 'The rules export file contains non-object entries' } - ), - WRONG_FORMAT: (formatError: string) => { - return i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.wrongFormat', - { - defaultMessage: 'The rules export file has wrong format: {formatError}', - values: { formatError }, - } - ); - }, -}; - export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createSuccess', { defaultMessage: 'Rules uploaded successfully' } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/translations.ts new file mode 100644 index 0000000000000..1e7988b596e42 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/translations.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FILE_UPLOAD_ERROR = { + CAN_NOT_READ: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.fileUploadError.canNotRead', + { defaultMessage: 'Failed to read file' } + ), + CAN_NOT_READ_WITH_REASON: (reason: string) => + i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.fileUploadError.canNotReadWithReason', + { + defaultMessage: 'An error occurred when reading file: {reason}', + values: { reason }, + } + ), + CAN_NOT_PARSE: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.fileUploadError.canNotParse', + { defaultMessage: 'Cannot parse the file as either a JSON file or NDJSON file' } + ), + TOO_LARGE_TO_PARSE: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.fileUploadError.tooLargeToParse', + { defaultMessage: 'This file is too large to parse' } + ), + NOT_ARRAY: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.fileUploadError.notArray', + { defaultMessage: 'The file content is not an array' } + ), + EMPTY: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.fileUploadError.empty', + { defaultMessage: 'The file is empty' } + ), + NOT_OBJECT: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.fileUploadError.notObject', + { defaultMessage: 'The file contains non-object entries' } + ), + WRONG_FORMAT: (formatError: string) => { + return i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.fileUploadError.wrongFormat', + { + defaultMessage: 'The file has wrong format: {formatError}', + values: { formatError }, + } + ); + }, +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts index 16d8f60043bcb..7003fe93e9e30 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts @@ -5,6 +5,13 @@ * 2.0. */ -import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { + RuleMigrationResourceData, + RuleMigrationTaskStats, +} from '../../../../../common/siem_migrations/model/rule_migration.gen'; export type OnMigrationCreated = (migrationStats: RuleMigrationTaskStats) => void; +export type OnResourcesCreated = () => void; +export type OnMissingResourcesFetched = (missingResources: RuleMigrationResourceData[]) => void; + +export type DataInputStep = 1 | 2 | 3; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts index 94082cf59d359..18a4ebe47bf8d 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts @@ -12,10 +12,15 @@ import type { CreateRuleMigrationRequestBody } from '../../../../../common/siem_ import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import { reducer, initialState } from './common/api_request_reducer'; -export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess', - { defaultMessage: 'Rules uploaded successfully' } +export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess.title', + { defaultMessage: 'Rule migration created successfully' } ); +export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS_DESCRIPTION = (rules: number) => + i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess.description', + { defaultMessage: '{rules} rules uploaded', values: { rules } } + ); export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.service.createRuleError', { defaultMessage: 'Failed to upload rules file' } @@ -36,7 +41,10 @@ export const useCreateMigration = (onSuccess: OnSuccess) => { const migrationId = await siemMigrations.rules.createRuleMigration(data); const stats = await siemMigrations.rules.getRuleMigrationStats(migrationId); - notifications.toasts.addSuccess(RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS); + notifications.toasts.addSuccess({ + title: RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS_TITLE, + text: RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS_DESCRIPTION(data.length), + }); onSuccess(stats); dispatch({ type: 'success' }); } catch (err) { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_missing_resources.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_missing_resources.ts new file mode 100644 index 0000000000000..a0679aa1e8bd2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_missing_resources.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useReducer } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { RuleMigrationResourceData } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { reducer, initialState } from './common/api_request_reducer'; + +export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.getMissingResourcesError', + { defaultMessage: 'Failed to fetch missing macros & lookups' } +); + +export type GetMissingResources = (migrationId: string) => void; +export type OnSuccess = (missingResources: RuleMigrationResourceData[]) => void; + +export const useGetMissingResources = (onSuccess: OnSuccess) => { + const { siemMigrations, notifications } = useKibana().services; + const [state, dispatch] = useReducer(reducer, initialState); + + const getMissingResources = useCallback( + (migrationId) => { + (async () => { + try { + dispatch({ type: 'start' }); + const missingResources = await siemMigrations.rules.getMissingResources(migrationId); + + onSuccess(missingResources); + dispatch({ type: 'success' }); + } catch (err) { + const apiError = err.body ?? err; + notifications.toasts.addError(apiError, { + title: RULES_DATA_INPUT_CREATE_MIGRATION_ERROR, + }); + dispatch({ type: 'error', error: apiError }); + } + })(); + }, + [siemMigrations.rules, notifications.toasts, onSuccess] + ); + + return { isLoading: state.loading, error: state.error, getMissingResources }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts index 8b692f07eb3cb..88c6b798e2f40 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts @@ -6,7 +6,7 @@ */ import useObservable from 'react-use/lib/useObservable'; -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; export const useLatestStats = () => { @@ -16,8 +16,12 @@ export const useLatestStats = () => { siemMigrations.rules.startPolling(); }, [siemMigrations.rules]); + const refreshStats = useCallback(() => { + siemMigrations.rules.getRuleMigrationsStats(); // this updates latestStats$ internally + }, [siemMigrations.rules]); + const latestStats$ = useMemo(() => siemMigrations.rules.getLatestStats$(), [siemMigrations]); const latestStats = useObservable(latestStats$, null); - return { data: latestStats ?? [], isLoading: latestStats === null }; + return { data: latestStats ?? [], isLoading: latestStats === null, refreshStats }; }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts new file mode 100644 index 0000000000000..eab3888422bae --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts @@ -0,0 +1,51 @@ +/* + * 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 { useCallback, useReducer } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { UpsertRuleMigrationResourcesRequestBody } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { reducer, initialState } from './common/api_request_reducer'; + +export const RULES_DATA_INPUT_UPSERT_MIGRATION_RESOURCES_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.upsertRuleMigrationResourcesError', + { defaultMessage: 'Failed to upload rule migration resources' } +); + +export type UpsertResources = ( + migrationId: string, + data: UpsertRuleMigrationResourcesRequestBody +) => void; +export type OnSuccess = () => void; + +export const useUpsertResources = (onSuccess: OnSuccess) => { + const { siemMigrations, notifications } = useKibana().services; + const [state, dispatch] = useReducer(reducer, initialState); + + const upsertResources = useCallback( + (migrationId, data) => { + (async () => { + try { + dispatch({ type: 'start' }); + await siemMigrations.rules.upsertMigrationResources(migrationId, data); + + onSuccess(); + dispatch({ type: 'success' }); + } catch (err) { + const apiError = err.body ?? err; + notifications.toasts.addError(apiError, { + title: RULES_DATA_INPUT_UPSERT_MIGRATION_RESOURCES_ERROR, + }); + dispatch({ type: 'error', error: apiError }); + } + })(); + }, + [siemMigrations.rules, notifications.toasts, onSuccess] + ); + + return { isLoading: state.loading, error: state.error, upsertResources }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index c13b0606d771d..75b7887db6525 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -13,11 +13,15 @@ import { TRACE_OPTIONS_SESSION_STORAGE_KEY, } from '@kbn/elastic-assistant/impl/assistant_context/constants'; import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen'; -import type { RuleMigrationTaskStats } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import type { + RuleMigrationResourceData, + RuleMigrationTaskStats, +} from '../../../../common/siem_migrations/model/rule_migration.gen'; import type { CreateRuleMigrationRequestBody, GetAllStatsRuleMigrationResponse, GetRuleMigrationStatsResponse, + UpsertRuleMigrationResourcesRequestBody, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants'; import type { StartPluginsDependencies } from '../../../types'; @@ -29,6 +33,8 @@ import { getRuleMigrationsStatsAll, startRuleMigration, type GetRuleMigrationsStatsAllParams, + getMissingResources, + upsertMigrationResources, } from '../api'; import type { RuleMigrationStats } from '../types'; import { getSuccessToast } from './success_notification'; @@ -99,6 +105,20 @@ export class SiemRulesMigrationsService { return migrationId as string; } + public async upsertMigrationResources( + migrationId: string, + body: UpsertRuleMigrationResourcesRequestBody + ): Promise { + if (body.length === 0) { + throw new Error(i18n.EMPTY_RULES_ERROR); + } + // Batching creation to avoid hitting the max payload size limit of the API + for (let i = 0; i < body.length; i += CREATE_MIGRATION_BODY_BATCH_SIZE) { + const bodyBatch = body.slice(i, i + CREATE_MIGRATION_BODY_BATCH_SIZE); + await upsertMigrationResources({ migrationId, body: bodyBatch }); + } + } + public async startRuleMigration(migrationId: string): Promise { const connectorId = this.connectorIdStorage.get(); if (!connectorId) { @@ -135,6 +155,10 @@ export class SiemRulesMigrationsService { return results; } + public async getMissingResources(migrationId: string): Promise { + return getMissingResources({ migrationId }); + } + private async getRuleMigrationsStatsWithRetry( params: GetRuleMigrationsStatsAllParams = {}, sleepSecs?: number diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts index 84ea909c06fb4..0c9ad11f4cce6 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts @@ -7,6 +7,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { RuleMigrationResourceData } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import { GetRuleMigrationResourcesMissingRequestParams, type GetRuleMigrationResourcesMissingResponse, @@ -47,7 +48,13 @@ export const registerSiemRuleMigrationsResourceGetMissingRoute = ( const options = { filters: { hasContent: false } }; const batches = ruleMigrationsClient.data.resources.searchBatches(migrationId, options); - const missingResources = await batches.all(); // TODO: return only type and name + + const missingResources: RuleMigrationResourceData[] = []; + let results = await batches.next(); + while (results.length) { + missingResources.push(...results.map(({ type, name }) => ({ type, name }))); + results = await batches.next(); + } return res.ok({ body: missingResources }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts index d8dc1bb168a72..77ed5e87084e9 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts @@ -11,6 +11,10 @@ import type { RuleMigrationsDataRulesClient } from '../rule_migrations_data_rule export const mockRuleMigrationsDataRulesClient = { create: jest.fn().mockResolvedValue(undefined), get: jest.fn().mockResolvedValue([]), + searchBatches: jest.fn().mockReturnValue({ + next: jest.fn().mockResolvedValue([]), + all: jest.fn().mockResolvedValue([]), + }), takePending: jest.fn().mockResolvedValue([]), saveCompleted: jest.fn().mockResolvedValue(undefined), saveError: jest.fn().mockResolvedValue(undefined), @@ -27,6 +31,10 @@ export const MockRuleMigrationsDataRulesClient = jest export const mockRuleMigrationsDataResourcesClient = { upsert: jest.fn().mockResolvedValue(undefined), get: jest.fn().mockResolvedValue(undefined), + searchBatches: jest.fn().mockReturnValue({ + next: jest.fn().mockResolvedValue([]), + all: jest.fn().mockResolvedValue([]), + }), }; export const MockRuleMigrationsDataResourcesClient = jest .fn() diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index a6ea5c9040e16..e81f7cdd5e6cb 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -92,6 +92,8 @@ export class RuleMigrationsTaskClient { const resourceRetriever = new RuleResourceRetriever(migrationId, this.data); const integrationRetriever = new IntegrationRetriever(this.data); + await resourceRetriever.initialize(); + const actionsClientChat = new ActionsClientChat(connectorId, actionsClient, this.logger); const model = await actionsClientChat.createModel({ signal: abortController.signal, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.test.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.test.ts index 51618d5f3ca13..1e02eec2315e7 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.test.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.test.ts @@ -5,176 +5,124 @@ * 2.0. */ -import { MAX_RECURSION_DEPTH, RuleResourceRetriever } from './rule_resource_retriever'; // Adjust path as needed +import { RuleResourceRetriever } from './rule_resource_retriever'; // Adjust path as needed import type { OriginalRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; -import { MockRuleMigrationsDataClient } from '../../data/__mocks__/mocks'; - -const mockRuleResourceIdentifier = jest.fn(); -const mockGetRuleResourceIdentifier = jest.fn((_: unknown) => mockRuleResourceIdentifier); -jest.mock('../../../../../../common/siem_migrations/rules/resources', () => ({ - getRuleResourceIdentifier: (params: unknown) => mockGetRuleResourceIdentifier(params), -})); +import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources'; +import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_client'; jest.mock('../../data/rule_migrations_data_service'); +jest.mock('../../../../../../common/siem_migrations/rules/resources'); + +const MockResourceIdentifier = ResourceIdentifier as jest.Mock; describe('RuleResourceRetriever', () => { let retriever: RuleResourceRetriever; - const mockRuleMigrationsDataClient = new MockRuleMigrationsDataClient(); - const migrationId = 'test-migration-id'; - const ruleQuery = 'rule-query'; - const originalRule = { query: ruleQuery } as OriginalRule; + let mockDataClient: jest.Mocked; + let mockResourceIdentifier: jest.Mocked; beforeEach(() => { - retriever = new RuleResourceRetriever(migrationId, mockRuleMigrationsDataClient); - mockRuleResourceIdentifier.mockReturnValue({ list: [], macro: [] }); - - mockRuleMigrationsDataClient.resources.get.mockImplementation( - async (_: string, type: string, names: string[]) => - names.map((name) => ({ type, name, content: `${name}-content` })) - ); - - mockRuleResourceIdentifier.mockImplementation((query) => { - if (query === ruleQuery) { - return { list: ['list1', 'list2'], macro: ['macro1'] }; - } - return { list: [], macro: [] }; - }); - - jest.clearAllMocks(); + mockDataClient = { + resources: { searchBatches: jest.fn().mockReturnValue({ next: jest.fn(() => []) }) }, + } as unknown as RuleMigrationsDataClient; + + retriever = new RuleResourceRetriever('mockMigrationId', mockDataClient); + + MockResourceIdentifier.mockImplementation(() => ({ + fromOriginalRule: jest.fn().mockReturnValue([]), + fromResources: jest.fn().mockReturnValue([]), + })); + mockResourceIdentifier = new MockResourceIdentifier( + 'splunk' + ) as jest.Mocked; }); - describe('getResources', () => { - it('should call resource identification', async () => { - await retriever.getResources(originalRule); + it('throws an error if initialize is not called before getResources', async () => { + const originalRule = { vendor: 'splunk' } as unknown as OriginalRule; - expect(mockGetRuleResourceIdentifier).toHaveBeenCalledWith(originalRule); - expect(mockRuleResourceIdentifier).toHaveBeenCalledWith(ruleQuery); - expect(mockRuleResourceIdentifier).toHaveBeenCalledWith('macro1-content'); - }); - - it('should retrieve resources', async () => { - const resources = await retriever.getResources(originalRule); - - expect(mockRuleMigrationsDataClient.resources.get).toHaveBeenCalledWith(migrationId, 'list', [ - 'list1', - 'list2', - ]); - expect(mockRuleMigrationsDataClient.resources.get).toHaveBeenCalledWith( - migrationId, - 'macro', - ['macro1'] - ); - - expect(resources).toEqual({ - list: [ - { type: 'list', name: 'list1', content: 'list1-content' }, - { type: 'list', name: 'list2', content: 'list2-content' }, - ], - macro: [{ type: 'macro', name: 'macro1', content: 'macro1-content' }], - }); - }); + await expect(retriever.getResources(originalRule)).rejects.toThrow( + 'initialize must be called before calling getResources' + ); + }); - it('should retrieve nested resources', async () => { - mockRuleResourceIdentifier.mockImplementation((query) => { - if (query === ruleQuery) { - return { list: ['list1', 'list2'], macro: ['macro1'] }; - } - if (query === 'macro1-content') { - return { list: ['list3'], macro: [] }; - } - return { list: [], macro: [] }; - }); - - const resources = await retriever.getResources(originalRule); - - expect(mockRuleMigrationsDataClient.resources.get).toHaveBeenCalledWith(migrationId, 'list', [ - 'list1', - 'list2', - ]); - expect(mockRuleMigrationsDataClient.resources.get).toHaveBeenCalledWith( - migrationId, - 'macro', - ['macro1'] - ); - expect(mockRuleMigrationsDataClient.resources.get).toHaveBeenCalledWith(migrationId, 'list', [ - 'list3', - ]); - - expect(resources).toEqual({ - list: [ - { type: 'list', name: 'list1', content: 'list1-content' }, - { type: 'list', name: 'list2', content: 'list2-content' }, - { type: 'list', name: 'list3', content: 'list3-content' }, - ], - macro: [{ type: 'macro', name: 'macro1', content: 'macro1-content' }], - }); - }); + it('returns an empty object if no matching resources are found', async () => { + const originalRule = { vendor: 'splunk' } as unknown as OriginalRule; - it('should handle missing macros', async () => { - mockRuleMigrationsDataClient.resources.get.mockImplementation( - async (_: string, type: string, names: string[]) => { - if (type === 'macro') { - return []; - } - return names.map((name) => ({ type, name, content: `${name}-content` })); - } - ); - - const resources = await retriever.getResources(originalRule); - - expect(resources).toEqual({ - list: [ - { type: 'list', name: 'list1', content: 'list1-content' }, - { type: 'list', name: 'list2', content: 'list2-content' }, - ], - }); - }); + // Mock the resource identifier to return no resources + mockResourceIdentifier.fromOriginalRule.mockReturnValue([]); + await retriever.initialize(); // Pretend initialize has been called - it('should handle missing lists', async () => { - mockRuleMigrationsDataClient.resources.get.mockImplementation( - async (_: string, type: string, names: string[]) => { - if (type === 'list') { - return []; - } - return names.map((name) => ({ type, name, content: `${name}-content` })); - } - ); - - const resources = await retriever.getResources(originalRule); - - expect(resources).toEqual({ - macro: [{ type: 'macro', name: 'macro1', content: 'macro1-content' }], - }); - }); + const result = await retriever.getResources(originalRule); + expect(result).toEqual({}); + }); - it('should not include resources with missing content', async () => { - mockRuleMigrationsDataClient.resources.get.mockImplementation( - async (_: string, type: string, names: string[]) => { - return names.map((name) => { - if (name === 'list1') { - return { type, name, content: '' }; - } - return { type, name, content: `${name}-content` }; - }); - } - ); - - const resources = await retriever.getResources(originalRule); - - expect(resources).toEqual({ - list: [{ type: 'list', name: 'list2', content: 'list2-content' }], - macro: [{ type: 'macro', name: 'macro1', content: 'macro1-content' }], - }); + it('returns matching macro and list resources', async () => { + const mockExistingResources = { + macro: { macro1: { name: 'macro1', type: 'macro' } }, + list: { list1: { name: 'list1', type: 'list' } }, + }; + // Inject existing resources manually + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (retriever as any).existingResources = mockExistingResources; + + const mockResourcesIdentified = [ + { name: 'macro1', type: 'macro' as const }, + { name: 'list1', type: 'list' as const }, + ]; + MockResourceIdentifier.mockImplementation(() => ({ + fromOriginalRule: jest.fn().mockReturnValue(mockResourcesIdentified), + fromResources: jest.fn().mockReturnValue([]), + })); + + const originalRule = { vendor: 'splunk' } as unknown as OriginalRule; + + const result = await retriever.getResources(originalRule); + expect(result).toEqual({ + macro: [{ name: 'macro1', type: 'macro' }], + list: [{ name: 'list1', type: 'list' }], }); + }); - it('should stop recursion after reaching MAX_RECURSION_DEPTH', async () => { - mockRuleResourceIdentifier.mockImplementation(() => { - return { list: [], macro: ['infinite-macro'] }; - }); - - const resources = await retriever.getResources(originalRule); - - expect(resources.macro?.length).toEqual(MAX_RECURSION_DEPTH); + it('handles nested resources properly', async () => { + const originalRule = { vendor: 'splunk' } as unknown as OriginalRule; + + const mockExistingResources = { + macro: { + macro1: { name: 'macro1', type: 'macro' }, + macro2: { name: 'macro2', type: 'macro' }, + }, + list: { + list1: { name: 'list1', type: 'list' }, + list2: { name: 'list2', type: 'list' }, + }, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (retriever as any).existingResources = mockExistingResources; + + const mockResourcesIdentifiedFromRule = [ + { name: 'macro1', type: 'macro' as const }, + { name: 'list1', type: 'list' as const }, + ]; + + const mockNestedResources = [ + { name: 'macro2', type: 'macro' as const }, + { name: 'list2', type: 'list' as const }, + ]; + + MockResourceIdentifier.mockImplementation(() => ({ + fromOriginalRule: jest.fn().mockReturnValue(mockResourcesIdentifiedFromRule), + fromResources: jest.fn().mockReturnValue([]).mockReturnValueOnce(mockNestedResources), + })); + + const result = await retriever.getResources(originalRule); + expect(result).toEqual({ + macro: [ + { name: 'macro1', type: 'macro' }, + { name: 'macro2', type: 'macro' }, + ], + list: [ + { name: 'list1', type: 'list' }, + { name: 'list2', type: 'list' }, + ], }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.ts index c6ed55ee48025..f654402b96d93 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/rule_resource_retriever.ts @@ -34,13 +34,13 @@ export class RuleResourceRetriever { const batches = this.dataClient.resources.searchBatches(this.migrationId, options); const existingRuleResources: ExistingResources = { macro: {}, list: {} }; - let resources = await batches.next(); - while (resources.length > 0) { + let resources; + do { + resources = await batches.next(); resources.forEach((resource) => { existingRuleResources[resource.type][resource.name] = resource; }); - resources = await batches.next(); - } + } while (resources.length > 0); this.existingResources = existingRuleResources; } From 4ea0ae5f47bea24dce7f899edc0ca69c93f34779 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 9 Dec 2024 19:13:11 +0000 Subject: [PATCH 29/48] [CI] Auto-commit changed files from 'yarn openapi:generate' --- .../common/api/quickstart_client.gen.ts | 23 +++++++++++++++++ .../services/security_solution_api.gen.ts | 25 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index b5d72fc1ef207..3487fdf81c0c9 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -368,6 +368,8 @@ import type { GetRuleMigrationResourcesRequestQueryInput, GetRuleMigrationResourcesRequestParamsInput, GetRuleMigrationResourcesResponse, + GetRuleMigrationResourcesMissingRequestParamsInput, + GetRuleMigrationResourcesMissingResponse, GetRuleMigrationStatsRequestParamsInput, GetRuleMigrationStatsResponse, GetRuleMigrationTranslationStatsRequestParamsInput, @@ -1471,6 +1473,24 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Identifies missing resources from all the rules of an existing SIEM rules migration + */ + async getRuleMigrationResourcesMissing(props: GetRuleMigrationResourcesMissingProps) { + this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationResourcesMissing`); + return this.kbnClient + .request({ + path: replaceParams( + '/internal/siem_migrations/rules/{migration_id}/resources/missing', + props.params + ), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Retrieves the stats of a SIEM rules migration using the migration id provided */ @@ -2423,6 +2443,9 @@ export interface GetRuleMigrationResourcesProps { query: GetRuleMigrationResourcesRequestQueryInput; params: GetRuleMigrationResourcesRequestParamsInput; } +export interface GetRuleMigrationResourcesMissingProps { + params: GetRuleMigrationResourcesMissingRequestParamsInput; +} export interface GetRuleMigrationStatsProps { params: GetRuleMigrationStatsRequestParamsInput; } diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index a6d0ac86a810c..30903c2f572b2 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -104,6 +104,7 @@ import { GetRuleMigrationResourcesRequestQueryInput, GetRuleMigrationResourcesRequestParamsInput, } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; +import { GetRuleMigrationResourcesMissingRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetRuleMigrationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetRuleMigrationTranslationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timeline/get_timeline_route.gen'; @@ -998,6 +999,27 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + /** + * Identifies missing resources from all the rules of an existing SIEM rules migration + */ + getRuleMigrationResourcesMissing( + props: GetRuleMigrationResourcesMissingProps, + kibanaSpace: string = 'default' + ) { + return supertest + .get( + routeWithNamespace( + replaceParams( + '/internal/siem_migrations/rules/{migration_id}/resources/missing', + props.params + ), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Retrieves the stats of a SIEM rules migration using the migration id provided */ @@ -1760,6 +1782,9 @@ export interface GetRuleMigrationResourcesProps { query: GetRuleMigrationResourcesRequestQueryInput; params: GetRuleMigrationResourcesRequestParamsInput; } +export interface GetRuleMigrationResourcesMissingProps { + params: GetRuleMigrationResourcesMissingRequestParamsInput; +} export interface GetRuleMigrationStatsProps { params: GetRuleMigrationStatsRequestParamsInput; } From 6c2bcb6ed054b9060086871fb40cb237ff5d678d Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 9 Dec 2024 20:46:34 +0100 Subject: [PATCH 30/48] fix type --- .../rule_migrations_data_resources_client.ts | 4 ++-- .../retrievers/rule_resource_retriever.ts | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts index cb697de7ffc11..97e51e9bafdb0 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts @@ -115,7 +115,7 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli } /** Returns batching functions to traverse all the migration resources search results */ - searchBatches( + searchBatches( migrationId: string, options: { scroll?: Duration; size?: number; filters?: RuleMigrationResourceFilters } = {} ) { @@ -123,7 +123,7 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli const query = this.getFilterQuery(migrationId, filters); const search = { query, sort: '_doc', scroll, size }; // sort by _doc to ensure consistent order try { - return this.getSearchBatches(search); + return this.getSearchBatches(search); } catch (error) { this.logger.error(`Error scrolling rule migration resources: ${error.message}`); throw error; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.ts index f654402b96d93..b89939e199e5a 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.ts @@ -13,12 +13,15 @@ import type { } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_client'; +export interface RuleMigrationDefinedResource extends RuleMigrationResource { + content: string; // ensures content exists +} export type RuleMigrationResources = Partial< - Record + Record >; interface ExistingResources { - macro: Record; - list: Record; + macro: Record; + list: Record; } export class RuleResourceRetriever { @@ -30,8 +33,10 @@ export class RuleResourceRetriever { ) {} public async initialize(): Promise { - const options = { filters: { hasContent: true } }; - const batches = this.dataClient.resources.searchBatches(this.migrationId, options); + const batches = this.dataClient.resources.searchBatches( + this.migrationId, + { filters: { hasContent: true } } + ); const existingRuleResources: ExistingResources = { macro: {}, list: {} }; let resources; @@ -54,8 +59,8 @@ export class RuleResourceRetriever { const resourceIdentifier = new ResourceIdentifier(originalRule.vendor); const resourcesIdentifiedFromRule = resourceIdentifier.fromOriginalRule(originalRule); - const macrosFound = new Map(); - const listsFound = new Map(); + const macrosFound = new Map(); + const listsFound = new Map(); resourcesIdentifiedFromRule.forEach((resource) => { const existingResource = existingResources[resource.type][resource.name]; if (existingResource) { From cd1af6177ef8672b81aded38bd62524658f63e9b Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 9 Dec 2024 20:48:36 +0100 Subject: [PATCH 31/48] migration retriever initialize --- .../rules/task/retrievers/rule_migrations_retriever.ts | 4 ++++ .../siem_migrations/rules/task/rule_migrations_task_client.ts | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_migrations_retriever.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_migrations_retriever.ts index 22c884fa4043b..9c02f31c820e5 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_migrations_retriever.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_migrations_retriever.ts @@ -20,4 +20,8 @@ export class RuleMigrationsRetriever { this.integrations = new IntegrationRetriever(dataClient); this.prebuiltRules = new PrebuiltRulesRetriever(dataClient); } + + public async initialize() { + await this.resources.initialize(); + } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index 1e5ef3faf50ee..edaa37612568b 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -95,8 +95,7 @@ export class RuleMigrationsTaskClient { }); const ruleMigrationsRetriever = new RuleMigrationsRetriever(this.data, migrationId); - - await resourceRetriever.initialize(); + await ruleMigrationsRetriever.initialize(); const actionsClientChat = new ActionsClientChat(connectorId, actionsClient, this.logger); const model = await actionsClientChat.createModel({ From acb1a664d095281aaf4acbb72e90dcefeded22f2 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 10 Dec 2024 12:45:54 +0100 Subject: [PATCH 32/48] createAgent function --- .../retrievers/rule_migrations_retriever.ts | 29 +++- .../rules/task/rule_migrations_task_client.ts | 126 ++++++++---------- .../lib/siem_migrations/rules/task/types.ts | 15 +-- 3 files changed, 83 insertions(+), 87 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_migrations_retriever.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_migrations_retriever.ts index 9c02f31c820e5..29852558cda48 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_migrations_retriever.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_migrations_retriever.ts @@ -5,23 +5,42 @@ * 2.0. */ +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_client'; import { IntegrationRetriever } from './integration_retriever'; import { PrebuiltRulesRetriever } from './prebuilt_rules_retriever'; import { RuleResourceRetriever } from './rule_resource_retriever'; +interface RuleMigrationsRetrieverDeps { + data: RuleMigrationsDataClient; + rules: RulesClient; + savedObjects: SavedObjectsClientContract; +} + export class RuleMigrationsRetriever { public readonly resources: RuleResourceRetriever; public readonly integrations: IntegrationRetriever; public readonly prebuiltRules: PrebuiltRulesRetriever; - constructor(dataClient: RuleMigrationsDataClient, migrationId: string) { - this.resources = new RuleResourceRetriever(migrationId, dataClient); - this.integrations = new IntegrationRetriever(dataClient); - this.prebuiltRules = new PrebuiltRulesRetriever(dataClient); + constructor(migrationId: string, private readonly clients: RuleMigrationsRetrieverDeps) { + this.resources = new RuleResourceRetriever(migrationId, this.clients.data); + this.integrations = new IntegrationRetriever(this.clients.data); + this.prebuiltRules = new PrebuiltRulesRetriever(this.clients.data); } public async initialize() { - await this.resources.initialize(); + await Promise.all([ + this.resources.initialize(), + // Populates the indices used for RAG searches on prebuilt rules and integrations. + this.clients.data.prebuiltRules.create({ + rulesClient: this.clients.rules, + soClient: this.clients.savedObjects, + }), + // Will use Fleet API client for integration retrieval as an argument once feature is available + this.clients.data.integrations.create(), + ]).catch((error) => { + throw new Error(`Failed to initialize RuleMigrationsRetriever: ${error}`); + }); } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index edaa37612568b..fe3e01fe84925 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -20,9 +20,8 @@ import type { MigrateRuleState } from './agent/types'; import { RuleMigrationsRetriever } from './retrievers'; import type { MigrationAgent, - RuleMigrationTaskPrepareParams, - RuleMigrationTaskRunParams, RuleMigrationTaskStartParams, + RuleMigrationTaskCreateAgentParams, RuleMigrationTaskStartResult, RuleMigrationTaskStopResult, } from './types'; @@ -63,83 +62,40 @@ export class RuleMigrationsTaskClient { return { exists: true, started: false }; } - const abortController = new AbortController(); - - // Retrieve agent from prepare and pass it to run right after without awaiting but using .then - this.prepare({ ...params, abortController }) - .then((agent) => this.run({ ...params, agent, abortController })) - .catch((error) => { - this.logger.error(`Error starting migration ID:${migrationId} with error:${error}`, error); - }); - - return { exists: true, started: true }; - } - - private async prepare({ - migrationId, - connectorId, - inferenceClient, - actionsClient, - rulesClient, - soClient, - abortController, - }: RuleMigrationTaskPrepareParams): Promise { - await Promise.all([ - // Populates the indices used for RAG searches on prebuilt rules and integrations. - await this.data.prebuiltRules.create({ rulesClient, soClient }), - // Will use Fleet API client for integration retrieval as an argument once feature is available - await this.data.integrations.create(), - ]).catch((error) => { - this.logger.error(`Error preparing RAG indices for migration ID:${migrationId}`, error); - throw error; - }); - - const ruleMigrationsRetriever = new RuleMigrationsRetriever(this.data, migrationId); - await ruleMigrationsRetriever.initialize(); - - const actionsClientChat = new ActionsClientChat(connectorId, actionsClient, this.logger); - const model = await actionsClientChat.createModel({ - signal: abortController.signal, - temperature: 0.05, + // run the migration without awaiting it to execute it in the background + this.run(params).catch((error) => { + this.logger.error(`Error executing migration ID:${migrationId}`, error); }); - const agent = getRuleMigrationAgent({ - connectorId, - model, - inferenceClient, - ruleMigrationsRetriever, - logger: this.logger, - }); - return agent; + return { exists: true, started: true }; } - private async run({ - migrationId, - agent, - invocationConfig, - abortController, - }: RuleMigrationTaskRunParams): Promise { + private async run(params: RuleMigrationTaskStartParams): Promise { + const { migrationId, invocationConfig } = params; if (this.migrationsRunning.has(migrationId)) { // This should never happen, but just in case throw new Error(`Task already running for migration ID:${migrationId} `); } this.logger.info(`Starting migration ID:${migrationId}`); + const abortController = new AbortController(); this.migrationsRunning.set(migrationId, { user: this.currentUser.username, abortController }); - const config: RunnableConfig = { - ...invocationConfig, - // signal: abortController.signal, // not working properly https://github.com/langchain-ai/langgraphjs/issues/319 - }; const abortPromise = abortSignalToPromise(abortController.signal); + const withAbortRace = async (task: Promise) => Promise.race([task, abortPromise.promise]); + + const sleep = async (seconds: number) => { + this.logger.debug(`Sleeping ${seconds}s for migration ID:${migrationId}`); + await withAbortRace(new Promise((resolve) => setTimeout(resolve, seconds * 1000))); + }; try { - const sleep = async (seconds: number) => { - this.logger.debug(`Sleeping ${seconds}s for migration ID:${migrationId}`); - await Promise.race([ - new Promise((resolve) => setTimeout(resolve, seconds * 1000)), - abortPromise.promise, - ]); + this.logger.debug(`Creating agent for migration ID:${migrationId}`); + const agent = await withAbortRace(this.createAgent({ ...params, abortController })); + + const config: RunnableConfig = { + ...invocationConfig, + // signal: abortController.signal, // not working properly https://github.com/langchain-ai/langgraphjs/issues/319 }; let isDone: boolean = false; @@ -155,10 +111,12 @@ export class RuleMigrationsTaskClient { try { const start = Date.now(); - const migrationResult: MigrateRuleState = await Promise.race([ - agent.invoke({ original_rule: ruleMigration.original_rule }, config), - abortPromise.promise, // workaround for the issue with the langGraph signal - ]); + const invocation = agent.invoke( + { original_rule: ruleMigration.original_rule }, + config + ); + // using withAbortRace is a workaround for the issue with the langGraph signal not working properly + const migrationResult = await withAbortRace(invocation); const duration = (Date.now() - start) / 1000; this.logger.debug( @@ -212,6 +170,38 @@ export class RuleMigrationsTaskClient { } } + private async createAgent({ + migrationId, + connectorId, + inferenceClient, + actionsClient, + rulesClient, + soClient, + abortController, + }: RuleMigrationTaskCreateAgentParams): Promise { + const actionsClientChat = new ActionsClientChat(connectorId, actionsClient, this.logger); + const model = await actionsClientChat.createModel({ + signal: abortController.signal, + temperature: 0.05, + }); + + const ruleMigrationsRetriever = new RuleMigrationsRetriever(migrationId, { + data: this.data, + rules: rulesClient, + savedObjects: soClient, + }); + await ruleMigrationsRetriever.initialize(); + + const agent = getRuleMigrationAgent({ + connectorId, + model, + inferenceClient, + ruleMigrationsRetriever, + logger: this.logger, + }); + return agent; + } + /** Updates all the rules in a migration to be re-executed */ public async updateToRetry(migrationId: string): Promise<{ updated: boolean }> { if (this.migrationsRunning.has(migrationId)) { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/types.ts index 7ac7e848ba80d..7ddb08f1e47d6 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/types.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/types.ts @@ -30,20 +30,7 @@ export interface RuleMigrationTaskStartParams { soClient: SavedObjectsClientContract; } -export interface RuleMigrationTaskPrepareParams { - migrationId: string; - connectorId: string; - inferenceClient: InferenceClient; - actionsClient: ActionsClient; - rulesClient: RulesClient; - soClient: SavedObjectsClientContract; - abortController: AbortController; -} - -export interface RuleMigrationTaskRunParams { - migrationId: string; - invocationConfig: RunnableConfig; - agent: MigrationAgent; +export interface RuleMigrationTaskCreateAgentParams extends RuleMigrationTaskStartParams { abortController: AbortController; } From 473447304c7d7b612a0f1af436d3a670c95ebb38 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 10 Dec 2024 16:47:02 +0100 Subject: [PATCH 33/48] accept ndjson extension --- .../data_input_flyout/data_input_flyout.tsx | 16 ++++++++++++---- .../macros_file_upload/macros_file_upload.tsx | 2 +- .../rules_file_upload/rules_file_upload.tsx | 2 +- .../rules/components/data_input_flyout/types.ts | 7 ++++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index 92479ce516977..ffc40c59d495a 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -23,7 +23,7 @@ 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 type { DataInputStep } from './types'; +import { DataInputStep } from './types'; import { MacrosDataInput } from './steps/macros/macros_data_input'; interface MissingResourcesIndexed { @@ -51,7 +51,7 @@ export const MigrationDataInputFlyout = React.memo(1); + const [dataInputStep, setDataInputStep] = useState(DataInputStep.Rules); const onMigrationCreated = useCallback((createdMigrationStats: RuleMigrationTaskStats) => { setMigrationStats(createdMigrationStats); @@ -71,13 +71,21 @@ export const MigrationDataInputFlyout = React.memo { - setDataInputStep(3); + setDataInputStep(DataInputStep.Lookups); }, []); return ( diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx index 06983ccd30f6e..b8d7022d9b454 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx @@ -60,7 +60,7 @@ export const MacrosFileUpload = React.memo( } - accept="application/json" + accept="application/json, application/x-ndjson" onChange={parseFile} display="large" aria-label="Upload logs sample file" diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx index d378028df507e..bec9182420073 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx @@ -62,7 +62,7 @@ export const RulesFileUpload = React.memo( } - accept="application/json" + accept="application/json, application/x-ndjson" onChange={parseFile} display="large" aria-label="Upload rules file" diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts index 7003fe93e9e30..b293a9394ba54 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts @@ -14,4 +14,9 @@ export type OnMigrationCreated = (migrationStats: RuleMigrationTaskStats) => voi export type OnResourcesCreated = () => void; export type OnMissingResourcesFetched = (missingResources: RuleMigrationResourceData[]) => void; -export type DataInputStep = 1 | 2 | 3; +export enum DataInputStep { + Rules = 1, + Macros = 2, + Lookups = 3, + End = 10, +} From 9e8af45a3c6f8e807bddeae7a5eb7cc93718e93c Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 12 Dec 2024 10:45:39 +0100 Subject: [PATCH 34/48] results panel --- config/serverless.security.yml | 1 + .../common/siem_migrations/constants.ts | 2 +- .../model/rule_migration.gen.ts | 44 +++-- .../model/rule_migration.schema.yaml | 53 +++++-- .../ai_connector/ai_connector_card.tsx | 13 +- .../panels/migration_progress_panel.tsx | 58 +++++-- .../panels/migration_result_panel.tsx | 145 +++++++++++++++-- .../start_migration/translations.ts | 16 +- .../data_input_flyout/data_input_flyout.tsx | 19 ++- .../{sub_step_wrapper.tsx => sub_step.tsx} | 9 +- .../steps/common/use_parse_file_input.ts | 4 +- .../data_input_flyout/steps/constants.ts | 13 ++ .../steps/lookups/lookups_data_input.tsx | 130 +++++++++++++++ .../sub_steps/lookups_file_upload/index.tsx | 96 +++++++++++ .../lookups_file_upload.tsx | 150 ++++++++++++++++++ .../lookups_file_upload/translations.ts | 17 ++ .../sub_steps/missing_lookups_list/index.tsx | 36 +++++ .../missing_lookups_list.tsx | 100 ++++++++++++ .../missing_lookups_list/translations.ts | 21 +++ .../steps/lookups/translations.ts | 19 +++ .../steps/macros/macros_data_input.tsx | 28 +--- .../copy_export_query/copy_export_query.tsx | 2 +- .../sub_steps/copy_export_query/index.tsx | 2 +- .../copy_export_query/translations.ts | 4 +- .../macros_file_upload/macros_file_upload.tsx | 8 +- .../macros_file_upload/translations.ts | 13 +- .../steps/rules/rules_data_input.tsx | 30 ++-- .../components/data_input_flyout/types.ts | 7 - .../translation_tab/index.tsx | 2 +- .../rules/components/rules_table/index.tsx | 11 +- .../rules/components/status_badge/index.tsx | 13 +- .../index.ts} | 26 +-- .../{ => translation_results}/translations.ts | 4 +- .../lib/siem_migrations/rules/api/stats.ts | 3 + .../rules/api/translation_stats.ts | 3 + .../data/rule_migrations_data_rules_client.ts | 100 ++++++++---- .../lib/siem_migrations/rules/data/search.ts | 4 +- .../match_prebuilt_rule.ts | 4 +- .../siem_migrations/rules/task/agent/state.ts | 4 +- .../agent/sub_graphs/translate_rule/graph.ts | 4 +- .../nodes/translate_rule/translate_rule.ts | 8 +- .../agent/sub_graphs/translate_rule/state.ts | 6 +- 42 files changed, 1027 insertions(+), 205 deletions(-) rename x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/{sub_step_wrapper.tsx => sub_step.tsx} (67%) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/constants.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/missing_lookups_list.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/translations.ts rename x-pack/plugins/security_solution/public/siem_migrations/rules/utils/{helpers.tsx => translation_results/index.ts} (54%) rename x-pack/plugins/security_solution/public/siem_migrations/rules/utils/{ => translation_results}/translations.ts (94%) diff --git a/config/serverless.security.yml b/config/serverless.security.yml index b9190df608540..2f70f292ac870 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -7,6 +7,7 @@ xpack.infra.enabled: false xpack.observabilityLogsExplorer.enabled: false xpack.observability.enabled: false xpack.observabilityAIAssistant.enabled: false +xpack.inventory.enabled: false xpack.search.notebooks.enabled: false xpack.searchPlayground.enabled: false xpack.searchInferenceEndpoints.enabled: false diff --git a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts index 531669608ed8b..c65c3cd9e28c8 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/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', diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 9fd3876e141a8..848f73864af6d 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/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/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 0a99bd5ce701f..654123131f605 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -236,23 +236,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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index e42834e85d488..1786c9cbee85c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx index 0527e1cfbdf17..dfb762eec4e33 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx @@ -6,24 +6,34 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiPanel, EuiProgress } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPanel, + EuiProgress, + EuiLoadingSpinner, + EuiIcon, +} from '@elastic/eui'; +import { AssistantAvatar } from '@kbn/elastic-assistant'; import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; import * as i18n from '../translations'; import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; +import { CardSubduedText } from '../../../common/card_subdued_text'; 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]); + const finished = migrationStats.rules.completed + migrationStats.rules.failed; + const progressValue = (finished / migrationStats.rules.total) * 100; + + const preparing = migrationStats.rules.pending === migrationStats.rules.total; return ( - +

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

@@ -31,12 +41,40 @@ export const MigrationProgressPanel = React.memo(
-

{i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION}

+ {i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION(migrationStats.rules.total)}
- - - + {preparing ? ( + + + + + + + {i18n.START_MIGRATION_CARD_PREPARING_DESCRIPTION} + + + + + + + ) : ( + + + + )}
); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx index b73b3cc8b4921..46422189df144 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import moment from 'moment'; import { EuiFlexGroup, @@ -14,18 +14,34 @@ import { EuiPanel, EuiHorizontalRule, EuiIcon, + EuiBasicTable, + EuiBadge, + EuiHealth, } from '@elastic/eui'; +import { Chart, BarSeries, Axis, Settings, Position, ScaleType } from '@elastic/charts'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; +import { + convertTranslationResultIntoColor, + convertTranslationResultIntoText, + statusToColorMap, +} from '../../../../../../../siem_migrations/rules/utils/translation_results'; +import type { RuleMigrationTranslationStats } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { useGetMigrationTranslationStats } from '../../../../../../../siem_migrations/rules/logic/use_get_migration_translation_stats'; +import { CenteredLoadingSpinner } from '../../../../../../../common/components/centered_loading_spinner'; import { SecuritySolutionLinkButton } from '../../../../../../../common/components/links'; import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; import * as i18n from '../translations'; +// import '@elastic/charts/dist/theme_light.css'; import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; +import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; export interface MigrationResultPanelProps { migrationStats: RuleMigrationStats; } export const MigrationResultPanel = React.memo(({ migrationStats }) => { + const { data: translationStats, isLoading: isLoadingTranslationStats } = + useGetMigrationTranslationStats(migrationStats.id); return ( @@ -64,17 +80,30 @@ export const MigrationResultPanel = React.memo(({ mig - - -

{'TODO: chart'}

+ + + {isLoadingTranslationStats ? ( + + ) : ( + translationStats && ( + <> + + + + ) + )} - - {i18n.VIEW_TRANSLATED_RULES_BUTTON} - + + + + {i18n.VIEW_TRANSLATED_RULES_BUTTON} + + +
@@ -85,3 +114,99 @@ export const MigrationResultPanel = React.memo(({ mig ); }); MigrationResultPanel.displayName = 'MigrationResultPanel'; + +const TranslationResultsChart = React.memo<{ + translationStats: RuleMigrationTranslationStats; +}>(({ translationStats }) => { + const data = [ + { category: 'Results', type: 'Full', value: translationStats.rules.success.result.full }, + { category: 'Results', type: 'Partial', value: translationStats.rules.success.result.partial }, + { + category: 'Results', + type: 'Untranslatable', + value: translationStats.rules.success.result.untranslatable, + }, + { category: 'Results', type: 'Failed', value: translationStats.rules.failed }, + ]; + + const colors = [ + statusToColorMap[RuleTranslationResult.FULL], + statusToColorMap[RuleTranslationResult.PARTIAL], + statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], + 'danger', + ]; + + return ( + + + + + ); +}); +TranslationResultsChart.displayName = 'TranslationResultsChart'; + +const TranslationResultsTable = React.memo<{ + translationStats: RuleMigrationTranslationStats; +}>(({ translationStats }) => { + const items = useMemo(() => { + return [ + { + id: 'full', + title: convertTranslationResultIntoText(RuleTranslationResult.FULL), + value: translationStats.rules.success.result.full, + color: statusToColorMap[RuleTranslationResult.FULL], + }, + { + id: 'partial', + title: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL), + value: translationStats.rules.success.result.partial, + color: statusToColorMap[RuleTranslationResult.PARTIAL], + }, + { + id: 'untranslatable', + title: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE), + value: translationStats.rules.success.result.untranslatable, + color: statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], + }, + { + id: 'failed', + title: 'Failed', + value: translationStats.rules.failed, + color: 'danger', + }, + ]; + }, [translationStats]); + + return ( + {value}, + }, + { + field: 'value', + name: 'Rules', + align: 'right', + }, + ]} + /> + ); +}); +TranslationResultsTable.displayName = 'TranslationResultsTable'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index bdb3f31842549..58146cbfa2271 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -88,12 +88,22 @@ export const START_MIGRATION_CARD_MIGRATION_TITLE = (number: number) => values: { number }, }); -export const START_MIGRATION_CARD_PROGRESS_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.progress.description', +export const START_MIGRATION_CARD_PROGRESS_DESCRIPTION = (totalRules: number) => + i18n.translate('xpack.securitySolution.onboarding.startMigration.progress.description', { + defaultMessage: `Processing migration of {totalRules} rules.`, + values: { totalRules }, + }); +export const START_MIGRATION_CARD_PREPARING_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.preparing.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.`, + defaultMessage: `Preparing environment for the AI powered migration.`, } ); +export const START_MIGRATION_CARD_PROCESSING = (rulesLeft: number) => + i18n.translate('xpack.securitySolution.onboarding.startMigration.processing', { + defaultMessage: `{rulesLeft} rules left...`, + values: { rulesLeft }, + }); export const START_MIGRATION_CARD_RESULT_TITLE = (number: number) => i18n.translate('xpack.securitySolution.onboarding.startMigration.result.title', { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index ffc40c59d495a..58a9872ed859b 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/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[]; @@ -87,6 +89,9 @@ export const MigrationDataInputFlyout = React.memo { setDataInputStep(DataInputStep.Lookups); }, []); + const onLookupsCreated = useCallback(() => { + setDataInputStep(DataInputStep.End); + }, []); return (
+ + + - + - + >(({ children }) => { +export const SubSteps = React.memo<{ steps: EuiStepProps[] }>(({ steps }) => { return ( - {children} + ); }); -SubStepsWrapper.displayName = 'SubStepsWrapper'; +SubSteps.displayName = 'SubSteps'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts +++ b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/constants.ts b/x-pack/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/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx new file mode 100644 index 0000000000000..2f40d6bf11737 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx @@ -0,0 +1,130 @@ +/* + * 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, useMemo, useState } from 'react'; +import type { 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'; + +interface LookupsDataInputSubStepsProps { + migrationStats: RuleMigrationTaskStats; + missingLookups: string[]; + onLookupsCreated: OnResourcesCreated; +} +interface LookupsDataInputProps + extends Omit { + dataInputStep: DataInputStep; + migrationStats?: RuleMigrationTaskStats; + missingLookups?: string[]; +} +export const LookupsDataInput = React.memo( + ({ dataInputStep, migrationStats, missingLookups, onLookupsCreated }) => { + 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, onLookupsCreated }) => { + const [subStep, setSubStep] = useState(1); + const [uploadedLookups, setUploadedLookups] = useState>({}); + const addUploadedLookups = useCallback((lookupNames: string[]) => { + setUploadedLookups((prevUploadedLookups) => ({ + ...prevUploadedLookups, + ...Object.fromEntries(lookupNames.map((lookupName) => [lookupName, true])), + })); + }, []); + + // Copy query step + const onCopied = useCallback(() => { + setSubStep(2); + }, []); + const copyStep = useMissingLookupsListStep({ + status: getStatus(1, subStep), + missingLookups, + uploadedLookups, + onCopied, + }); + + // Upload macros step + const onLookupsCreatedStep = useCallback(() => { + onLookupsCreated(); + setSubStep(END); + }, [onLookupsCreated]); + + const uploadStep = useLookupsFileUploadStep({ + status: getStatus(2, subStep), + migrationStats, + missingLookups, + addUploadedLookups, + onLookupsCreated: onLookupsCreatedStep, + }); + + const steps = useMemo(() => [copyStep, uploadStep], [copyStep, uploadStep]); + + return ; + } +); +LookupsDataInputSubSteps.displayName = 'LookupsDataInputActive'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx b/x-pack/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..c1bcc0e98f26b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx @@ -0,0 +1,96 @@ +/* + * 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 { ResourceIdentifier } from '../../../../../../../../../common/siem_migrations/rules/resources'; +import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources'; +import type { + RuleMigrationResourceData, + RuleMigrationTaskStats, +} from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { OnResourcesCreated } from '../../../../types'; +import { LookupsFileUpload } from './lookups_file_upload'; +import * as i18n from './translations'; + +export interface RulesFileUploadStepProps { + status: EuiStepStatus; + migrationStats: RuleMigrationTaskStats; + missingLookups: string[]; + addUploadedLookups: (lookups: string[]) => void; + onLookupsCreated: OnResourcesCreated; +} +export const useLookupsFileUploadStep = ({ + status, + migrationStats, + missingLookups, + addUploadedLookups, + onLookupsCreated, +}: RulesFileUploadStepProps): EuiStepProps => { + const { upsertResources, isLoading, error } = useUpsertResources(onLookupsCreated); + + const upsertMigrationResources = useCallback( + (lookupsFromFile: RuleMigrationResourceData[]) => { + const lookupsIndexed: Record = Object.fromEntries( + lookupsFromFile.map((lookup) => [lookup.name, lookup]) + ); + const resourceIdentifier = new ResourceIdentifier('splunk'); + const lookupsToUpsert: RuleMigrationResourceData[] = []; + let missingLookupsIt: string[] = missingLookups; + + while (missingLookupsIt.length > 0) { + const lookups: RuleMigrationResourceData[] = []; + missingLookupsIt.forEach((lookupName) => { + const lookup = lookupsIndexed[lookupName]; + if (lookup) { + lookups.push(lookup); + } else { + // Macro missing from file + } + }); + lookupsToUpsert.push(...lookups); + + missingLookupsIt = resourceIdentifier + .fromResources(lookups) + .reduce((acc, resource) => { + if (resource.type === 'list') { + acc.push(resource.name); + } + return acc; + }, []); + } + + if (lookupsToUpsert.length === 0) { + return; // No missing lookups provided + } + upsertResources(migrationStats.id, lookupsToUpsert); + }, + [upsertResources, migrationStats, missingLookups] + ); + + 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/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/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..a1e7704f48f10 --- /dev/null +++ b/x-pack/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,150 @@ +/* + * 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 { + EuiButton, + EuiFilePicker, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiText, +} from '@elastic/eui'; +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 onFileLoaded = useCallback((name: string, content: string) => { + setLookupResources((current) => [...current, { type: 'list', name, content }]); + }, []); + + const createLookups = useCallback(() => { + createResources(lookupResources); + }, [createResources, lookupResources]); + + const [isParsing, setIsParsing] = useState(false); + const [fileError, setError] = useState(); + + const parseFile = useCallback( + async (files: FileList | null) => { + if (!files) { + return; + } + + setError(undefined); + + for (const file of files) { + 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 fileContent = e.target?.result as string | undefined; + + if (fileContent == null) { + setError(FILE_UPLOAD_ERROR.CAN_NOT_READ); + return; + } + + if (fileContent === '' && 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 + setError(FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + return; + } + + try { + onFileLoaded(file.name, fileContent); + } catch (err) { + setError(err.message); + } + }; + + const handleReaderError = function () { + const message = reader.error?.message; + if (message) { + setError(FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); + } else { + setError(FILE_UPLOAD_ERROR.CAN_NOT_READ); + } + }; + + reader.onerror = handleReaderError; + reader.onabort = handleReaderError; + + reader.readAsText(file); + } + }, + [onFileLoaded] + ); + + const error = useMemo(() => { + if (apiError) { + return apiError; + } + return fileError; + }, [apiError, fileError]); + + return ( + + + + {error} + + } + isInvalid={error != null} + fullWidth + > + + + {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} + /> + + + + + + + {'upload IT!'} + + + + + + ); + } +); +LookupsFileUpload.displayName = 'LookupsFileUpload'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts b/x-pack/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..7d8f34c4078c4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts @@ -0,0 +1,17 @@ +/* + * 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' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx b/x-pack/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..e081bfc0e4bef --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { MissingLookupsList } from './missing_lookups_list'; +import * as i18n from './translations'; + +export interface MissingLookupsListStepProps { + status: EuiStepStatus; + onCopied: () => void; + missingLookups: string[]; + uploadedLookups: Record; +} +export const useMissingLookupsListStep = ({ + status, + onCopied, + missingLookups, + uploadedLookups, +}: MissingLookupsListStepProps): EuiStepProps => { + return { + title: i18n.LOOKUPS_DATA_INPUT_COPY_TITLE, + status, + children: ( + + ), + }; +}; diff --git a/x-pack/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/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..bb0049961fe7b --- /dev/null +++ b/x-pack/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,100 @@ +/* + * 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 { + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import * as i18n from './translations'; + +const scrollPanelCss = css` + max-height: 200px; + overflow-y: auto; +`; + +interface MissingLookupsListProps { + missingLookups: string[]; + uploadedLookups: Record; + onCopied: () => void; +} +export const MissingLookupsList = React.memo( + ({ missingLookups, uploadedLookups, onCopied }) => { + return ( + <> + + + {missingLookups.map((lookupName) => { + return ( + + + + {uploadedLookups[lookupName] ? ( + + ) : ( + + )} + + + {lookupName} + + + + {(copy) => ( + + )} + + + + + ); + })} + + + + + {i18n.MISSING_LOOKUPS_DESCRIPTION} + + + ); + } +); +MissingLookupsList.displayName = 'MissingLookupsList'; + +interface CopyButtonProps { + lookupName: string; + onCopied: () => void; + copy: () => void; +} +const CopyButton = React.memo(({ lookupName, onCopied, copy }) => { + const onClick = useCallback(() => { + copy(); + onCopied(); + }, [copy, onCopied]); + return ( + + ); +}); +CopyButton.displayName = 'CopyButton'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts b/x-pack/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..aa0e55a16f3d7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts @@ -0,0 +1,21 @@ +/* + * 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.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.', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/translations.ts b/x-pack/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/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx index f19e704b96710..f586ad21e25f4 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx @@ -6,26 +6,18 @@ */ 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[]; @@ -47,7 +39,7 @@ export const MacrosDataInput = React.memo( onMissingResourcesFetched, }) => { const dataInputStatus = useMemo( - () => getStatus(DataInputStepNumber, dataInputStep), + () => getStatus(DataInputStep.Macros, dataInputStep), [dataInputStep] ); @@ -59,7 +51,7 @@ export const MacrosDataInput = React.memo( @@ -130,11 +122,7 @@ export const MacrosDataInputSubSteps = React.memo( [copyStep, uploadStep, resourcesStep] ); - return ( - - - - ); + return ; } ); MacrosDataInputSubSteps.displayName = 'MacrosDataInputActive'; diff --git a/x-pack/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/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/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/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/index.tsx +++ b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/copy_export_query/translations.ts +++ b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx index b8d7022d9b454..5cea4afdb8537 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx @@ -25,8 +25,8 @@ export const MacrosFileUpload = React.memo( ({ 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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/translations.ts +++ b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx index acc22a030b02f..0c919a2db7a5d 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx +++ b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts index b293a9394ba54..1e5a8a0f7028c 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts +++ b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/index.tsx index a80480b8837bb..053a4e779619f 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/index.tsx @@ -28,7 +28,7 @@ import * as i18n from './translations'; import { convertTranslationResultIntoColor, convertTranslationResultIntoText, -} from '../../../utils/helpers'; +} from '../../../utils/translation_results'; interface TranslationTabProps { ruleMigration: RuleMigration; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx index 106e7ba514d3f..66c99ca7b82ec 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx @@ -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 ( @@ -260,7 +257,7 @@ export const MigrationRulesTable: React.FC = React.mem = { - full: euiColorVis0, - partial: euiColorVis7, - untranslatable: euiColorVis9, -}; - interface StatusBadgeProps { value?: RuleMigrationTranslationResult; installedRuleId?: string; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts similarity index 54% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx rename to x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts index fe3fbf9945077..57fa9f3327166 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts @@ -5,21 +5,27 @@ * 2.0. */ -import { - RuleMigrationTranslationResultEnum, - type RuleMigrationTranslationResult, -} from '../../../../common/siem_migrations/model/rule_migration.gen'; +import { euiLightVars } from '@kbn/ui-theme'; +import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; +import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; +const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars; +export const statusToColorMap: Record = { + [RuleTranslationResult.FULL]: euiColorVis0, + [RuleTranslationResult.PARTIAL]: euiColorVis7, + [RuleTranslationResult.UNTRANSLATABLE]: euiColorVis9, +}; + export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => { switch (status) { - case RuleMigrationTranslationResultEnum.full: + case RuleTranslationResult.FULL: return 'primary'; - case RuleMigrationTranslationResultEnum.partial: + case RuleTranslationResult.PARTIAL: return 'warning'; - case RuleMigrationTranslationResultEnum.untranslatable: + case RuleTranslationResult.UNTRANSLATABLE: return 'danger'; default: @@ -29,13 +35,13 @@ export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslat export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => { switch (status) { - case RuleMigrationTranslationResultEnum.full: + case RuleTranslationResult.FULL: return i18n.SIEM_TRANSLATION_RESULT_FULL_LABEL; - case RuleMigrationTranslationResultEnum.partial: + case RuleTranslationResult.PARTIAL: return i18n.SIEM_TRANSLATION_RESULT_PARTIAL_LABEL; - case RuleMigrationTranslationResultEnum.untranslatable: + case RuleTranslationResult.UNTRANSLATABLE: return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL; default: diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/translations.ts similarity index 94% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts rename to x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/translations.ts index 366ad435c61b4..f0f38cfc61481 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const SIEM_TRANSLATION_RESULT_FULL_LABEL = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.translationResult.full', { - defaultMessage: 'Fully translated', + defaultMessage: 'Translated', } ); @@ -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/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts index 5fb7d9e0525c1..4657f2516181c 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts +++ b/x-pack/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/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts index 4f9d12385e32d..ede4ccbeaa6d7 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts +++ b/x-pack/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/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 8cdc776f631b0..154aec3135f7f 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -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 { ElasticRule, RuleMigration, @@ -123,19 +126,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; @@ -263,8 +261,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 }) @@ -273,16 +278,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, }, }; } @@ -292,10 +303,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' } }, }; @@ -306,18 +314,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 ?? '', }; } @@ -328,10 +334,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient migrationIds: { terms: { field: 'migration_id', order: { createdAt: 'asc' } }, 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' } }, }, @@ -350,16 +353,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/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts index 282f783671fdc..3bd8da066a45f 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts +++ b/x-pack/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/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts index ea403c5c4ffa7..93f5cc3dbca1c 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts @@ -6,7 +6,7 @@ */ 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'; @@ -55,7 +55,7 @@ export const getMatchPrebuiltRuleNode = id: matchedRule.installedRuleId, prebuilt_rule_id: matchedRule.rule_id, }, - translation_result: SiemMigrationRuleTranslationResult.FULL, + translation_result: RuleTranslationResult.FULL, }; } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts index edd33e2ec69b6..0a3955631019d 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts +++ b/x-pack/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, @@ -36,7 +36,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/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts index 267a5bb0dd520..f715b108be09d 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts +++ b/x-pack/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 { getFixQueryErrorsNode } from './nodes/fix_query_errors'; import { getRetrieveIntegrationsNode } from './nodes/retrieve_integrations'; import { getTranslateRuleNode } from './nodes/translate_rule'; @@ -54,7 +54,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/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts index 85f5e7279d2b9..344e0fb9d3a3b 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts +++ b/x-pack/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 { SIEM_RULE_MIGRATION_CIM_ECS_MAP } from './cim_ecs_map'; @@ -63,9 +63,9 @@ export const getTranslateRuleNode = ({ }; }; -const getTranslationResult = (esqlQuery: string): SiemMigrationRuleTranslationResult => { +const getTranslationResult = (esqlQuery: string): RuleTranslationResult => { if (esqlQuery.match(/\[(macro|lookup):[\s\S]*\]/)) { - return SiemMigrationRuleTranslationResult.PARTIAL; + return RuleTranslationResult.PARTIAL; } - return SiemMigrationRuleTranslationResult.FULL; + return RuleTranslationResult.FULL; }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts index ac8799cb09d74..6612188b7b0c7 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts +++ b/x-pack/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, @@ -42,9 +42,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), From ae2a6b620897c2c3732fa811e6598592ef597f59 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 12 Dec 2024 10:58:11 +0100 Subject: [PATCH 35/48] address comments --- .../public/siem_migrations/rules/api/index.ts | 8 ++++---- .../steps/macros/sub_steps/macros_file_upload/index.tsx | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts index 0a7e3bb811977..17aa00dd0f01e 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -92,12 +92,12 @@ export const createRuleMigration = async ({ }; export interface GetRuleMigrationMissingResourcesParams { - /** `id` of the migration to start */ + /** `id` of the migration to get missing resources for */ migrationId: string; /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; } -/** Starts a new migration with the provided rules. */ +/** Retrieves all missing resources of a specific migration. */ export const getMissingResources = async ({ migrationId, signal, @@ -109,14 +109,14 @@ export const getMissingResources = async ({ }; export interface UpsertResourcesParams { - /** Optional `id` of migration to add the rules to. */ + /** Optional `id` of migration to add the resources to. */ migrationId: string; /** The body containing the `connectorId` to use for the migration */ body: UpsertRuleMigrationResourcesRequestBody; /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; } -/** Starts a new migration with the provided rules. */ +/** Updates or creates resources for a specific migration. */ export const upsertMigrationResources = async ({ migrationId, body, diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx index bca72e77487fb..f2353e3f0276a 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx @@ -87,7 +87,6 @@ export const useMacrosFileUploadStep = ({ ), From 782a39aff5f86e54acf5975c5364a6d284d54b33 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 13 Dec 2024 16:49:57 +0100 Subject: [PATCH 36/48] implement mark lookup as emtpy --- .../common/siem_migrations/constants.ts | 2 + .../steps/lookups/lookups_data_input.tsx | 35 +++-- .../sub_steps/lookups_file_upload/index.tsx | 49 ++----- .../lookups_file_upload.tsx | 124 +++++++++--------- .../lookups_file_upload/translations.ts | 17 +++ .../sub_steps/missing_lookups_list/index.tsx | 31 ++++- .../missing_lookups_list.tsx | 109 +++++++++++---- .../missing_lookups_list/translations.ts | 9 ++ .../service/hooks/use_upsert_resources.ts | 4 +- .../rules/api/resources/upsert.ts | 1 + 10 files changed, 238 insertions(+), 143 deletions(-) diff --git a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts index c65c3cd9e28c8..88ed777c21d69 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts @@ -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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx index 2f40d6bf11737..4214c4272c7c1 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx @@ -14,8 +14,12 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; +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'; @@ -24,6 +28,9 @@ 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[]; @@ -89,37 +96,43 @@ type SubStep = 1 | 2 | typeof END; export const LookupsDataInputSubSteps = React.memo( ({ migrationStats, missingLookups, onLookupsCreated }) => { const [subStep, setSubStep] = useState(1); - const [uploadedLookups, setUploadedLookups] = useState>({}); - const addUploadedLookups = useCallback((lookupNames: string[]) => { + const [uploadedLookups, setUploadedLookups] = useState({}); + + const addUploadedLookups = useCallback((lookups) => { setUploadedLookups((prevUploadedLookups) => ({ ...prevUploadedLookups, - ...Object.fromEntries(lookupNames.map((lookupName) => [lookupName, true])), + ...Object.fromEntries( + lookups.map((lookup) => [lookup.name, lookup.content ?? EMPTY_RESOURCE_PLACEHOLDER]) + ), })); }, []); + useEffect(() => { + if (missingLookups.every((lookupName) => uploadedLookups[lookupName])) { + setSubStep(END); + onLookupsCreated(); + } + }, [uploadedLookups, missingLookups, onLookupsCreated]); + // Copy query step const onCopied = useCallback(() => { setSubStep(2); }, []); const copyStep = useMissingLookupsListStep({ status: getStatus(1, subStep), + migrationStats, missingLookups, uploadedLookups, + addUploadedLookups, onCopied, }); // Upload macros step - const onLookupsCreatedStep = useCallback(() => { - onLookupsCreated(); - setSubStep(END); - }, [onLookupsCreated]); - const uploadStep = useLookupsFileUploadStep({ status: getStatus(2, subStep), migrationStats, missingLookups, addUploadedLookups, - onLookupsCreated: onLookupsCreatedStep, }); const steps = useMemo(() => [copyStep, uploadStep], [copyStep, uploadStep]); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx index c1bcc0e98f26b..f15413768b9a7 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/index.tsx @@ -7,69 +7,36 @@ import React, { useCallback, useMemo } from 'react'; import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; -import { ResourceIdentifier } from '../../../../../../../../../common/siem_migrations/rules/resources'; import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources'; import type { RuleMigrationResourceData, RuleMigrationTaskStats, } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; -import type { OnResourcesCreated } from '../../../../types'; -import { LookupsFileUpload } from './lookups_file_upload'; +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: (lookups: string[]) => void; - onLookupsCreated: OnResourcesCreated; + addUploadedLookups: AddUploadedLookups; } export const useLookupsFileUploadStep = ({ status, migrationStats, - missingLookups, addUploadedLookups, - onLookupsCreated, }: RulesFileUploadStepProps): EuiStepProps => { - const { upsertResources, isLoading, error } = useUpsertResources(onLookupsCreated); + const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups); const upsertMigrationResources = useCallback( (lookupsFromFile: RuleMigrationResourceData[]) => { - const lookupsIndexed: Record = Object.fromEntries( - lookupsFromFile.map((lookup) => [lookup.name, lookup]) - ); - const resourceIdentifier = new ResourceIdentifier('splunk'); - const lookupsToUpsert: RuleMigrationResourceData[] = []; - let missingLookupsIt: string[] = missingLookups; - - while (missingLookupsIt.length > 0) { - const lookups: RuleMigrationResourceData[] = []; - missingLookupsIt.forEach((lookupName) => { - const lookup = lookupsIndexed[lookupName]; - if (lookup) { - lookups.push(lookup); - } else { - // Macro missing from file - } - }); - lookupsToUpsert.push(...lookups); - - missingLookupsIt = resourceIdentifier - .fromResources(lookups) - .reduce((acc, resource) => { - if (resource.type === 'list') { - acc.push(resource.name); - } - return acc; - }, []); - } - - if (lookupsToUpsert.length === 0) { - return; // No missing lookups provided + if (lookupsFromFile.length === 0) { + return; // No lookups provided } - upsertResources(migrationStats.id, lookupsToUpsert); + upsertResources(migrationStats.id, lookupsFromFile); }, - [upsertResources, migrationStats, missingLookups] + [upsertResources, migrationStats] ); const uploadStepStatus = useMemo(() => { diff --git a/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx index a1e7704f48f10..048d1e38ed09d 100644 --- a/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx @@ -27,16 +27,15 @@ export const LookupsFileUpload = React.memo( ({ createResources, apiError, isLoading }) => { const [lookupResources, setLookupResources] = useState([]); - const onFileLoaded = useCallback((name: string, content: string) => { - setLookupResources((current) => [...current, { type: 'list', name, content }]); - }, []); - const createLookups = useCallback(() => { createResources(lookupResources); }, [createResources, lookupResources]); const [isParsing, setIsParsing] = useState(false); - const [fileError, setError] = useState(); + const [fileErrors, setErrors] = useState([]); + const addError = useCallback((error: string) => { + setErrors((current) => [...current, error]); + }, []); const parseFile = useCallback( async (files: FileList | null) => { @@ -44,72 +43,79 @@ export const LookupsFileUpload = React.memo( return; } - setError(undefined); - - for (const file of files) { - 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 fileContent = e.target?.result as string | undefined; - - if (fileContent == null) { - setError(FILE_UPLOAD_ERROR.CAN_NOT_READ); - return; - } - - if (fileContent === '' && 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 - setError(FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); - return; - } - - try { - onFileLoaded(file.name, fileContent); - } catch (err) { - setError(err.message); - } - }; - - const handleReaderError = function () { - const message = reader.error?.message; - if (message) { - setError(FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); - } else { - setError(FILE_UPLOAD_ERROR.CAN_NOT_READ); - } - }; - - reader.onerror = handleReaderError; - reader.onabort = handleReaderError; - - reader.readAsText(file); - } + setErrors([]); + setLookupResources([]); + + 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]); }, - [onFileLoaded] + [addError] ); - const error = useMemo(() => { + const errors = useMemo(() => { if (apiError) { - return apiError; + return [apiError]; } - return fileError; - }, [apiError, fileError]); + return fileErrors; + }, [apiError, fileErrors]); return ( ( {error} - } - isInvalid={error != null} + ))} + isInvalid={errors.length > 0} fullWidth > ( - {'upload IT!'} + {i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_BUTTON} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts index 7d8f34c4078c4..492f51309ca53 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/translations.ts @@ -15,3 +15,20 @@ 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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx index e081bfc0e4bef..d20c997838206 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx @@ -5,23 +5,42 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; -import { MissingLookupsList } from './missing_lookups_list'; +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; - onCopied: () => void; + migrationStats: RuleMigrationTaskStats; missingLookups: string[]; - uploadedLookups: Record; + uploadedLookups: UploadedLookups; + addUploadedLookups: AddUploadedLookups; + onCopied: () => void; } export const useMissingLookupsListStep = ({ status, - onCopied, + migrationStats, missingLookups, uploadedLookups, + addUploadedLookups, + onCopied, }: MissingLookupsListStepProps): EuiStepProps => { + const { upsertResources, isLoading } = useUpsertResources(addUploadedLookups); + + const clearLookup = useCallback( + (lookupName: string) => { + upsertResources(migrationStats.id, [ + { type: 'list', name: lookupName, content: EMPTY_RESOURCE_PLACEHOLDER }, + ]); + }, + [upsertResources, migrationStats] + ); + return { title: i18n.LOOKUPS_DATA_INPUT_COPY_TITLE, status, @@ -30,6 +49,8 @@ export const useMissingLookupsListStep = ({ onCopied={onCopied} missingLookups={missingLookups} uploadedLookups={uploadedLookups} + clearLookup={clearLookup} + isLoading={isLoading} /> ), }; diff --git a/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/missing_lookups_list.tsx index bb0049961fe7b..9cec751e563a8 100644 --- a/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/missing_lookups_list.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { css } from '@emotion/css'; import { EuiButtonIcon, EuiCopy, @@ -15,8 +16,11 @@ import { EuiPanel, EuiSpacer, EuiText, + EuiToolTip, + useEuiTheme, } from '@elastic/eui'; -import { css } from '@emotion/css'; +import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../../../common/siem_migrations/constants'; +import type { UploadedLookups } from '../../lookups_data_input'; import * as i18n from './translations'; const scrollPanelCss = css` @@ -26,14 +30,17 @@ const scrollPanelCss = css` interface MissingLookupsListProps { missingLookups: string[]; - uploadedLookups: Record; + uploadedLookups: UploadedLookups; + clearLookup: (lookupsName: string) => void; + isLoading: boolean; onCopied: () => void; } export const MissingLookupsList = React.memo( - ({ missingLookups, uploadedLookups, onCopied }) => { + ({ missingLookups, uploadedLookups, clearLookup, isLoading, onCopied }) => { + const { euiTheme } = useEuiTheme(); return ( <> - + {missingLookups.map((lookupName) => { return ( @@ -46,21 +53,41 @@ export const MissingLookupsList = React.memo( > {uploadedLookups[lookupName] ? ( - + ) : ( )} - {lookupName} + + {lookupName} + {(copy) => ( - + )} + + + ); @@ -77,24 +104,56 @@ export const MissingLookupsList = React.memo( ); MissingLookupsList.displayName = 'MissingLookupsList'; -interface CopyButtonProps { +interface CopyLookupNameButtonProps { lookupName: string; onCopied: () => void; copy: () => void; } -const CopyButton = React.memo(({ lookupName, onCopied, copy }) => { - const onClick = useCallback(() => { - copy(); - onCopied(); - }, [copy, onCopied]); - return ( - - ); -}); -CopyButton.displayName = 'CopyButton'; +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 }) => { + const button = useMemo( + () => ( + clearLookup(lookupName)} + iconType="cross" + color="text" + aria-label={`${i18n.CLEAR_EMPTY_LOOKUP_TOOLTIP} ${lookupName}`} + data-test-subj="lookupNameClear" + isDisabled={isDisabled} + /> + ), + [clearLookup, isDisabled, lookupName] + ); + if (isDisabled) { + return button; + } + return {button}; + } +); +ClearLookupButton.displayName = 'ClearLookupButton'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts index aa0e55a16f3d7..4c0a4dc36a7eb 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts @@ -19,3 +19,12 @@ export const MISSING_LOOKUPS_DESCRIPTION = i18n.translate( '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.copyLookupNameTooltip', + { defaultMessage: 'Copy lookup name' } +); +export const CLEAR_EMPTY_LOOKUP_TOOLTIP = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.clearEmptyLookupTooltip', + { defaultMessage: 'Mark the lookup as empty' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts index eab3888422bae..5ed8541a4af49 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_upsert_resources.ts +++ b/x-pack/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/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts index 9557c5cfd652f..fde332aefbd3f 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts +++ b/x-pack/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( { From fe8736c282fee839c83c31c5bfc26610628ecbe3 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:57:46 +0000 Subject: [PATCH 37/48] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../start_migration/panels/migration_progress_panel.tsx | 2 +- .../start_migration/panels/migration_result_panel.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx index dfb762eec4e33..e7001ba686074 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx index 46422189df144..c105099af543d 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx @@ -15,14 +15,12 @@ import { EuiHorizontalRule, EuiIcon, EuiBasicTable, - EuiBadge, EuiHealth, } from '@elastic/eui'; -import { Chart, BarSeries, Axis, Settings, Position, ScaleType } from '@elastic/charts'; +import { Chart, BarSeries, Settings, ScaleType } from '@elastic/charts'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; import { - convertTranslationResultIntoColor, convertTranslationResultIntoText, statusToColorMap, } from '../../../../../../../siem_migrations/rules/utils/translation_results'; From d9fbdce6c63b687222e83136d739dd282ee4ddce Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 16 Dec 2024 12:33:23 +0100 Subject: [PATCH 38/48] move code to siem_migrations public directory --- .../components/hooks/use_url_detail.ts | 12 +- .../panels/migration_progress_panel.tsx | 83 ------- .../panels/migration_ready_panel.tsx | 68 ------ .../panels/migration_result_panel.tsx | 212 ------------------ .../panels/upload_rules_panel.styles.ts | 19 -- .../panels/upload_rules_panel.tsx | 80 ------- .../start_migration/start_migration_card.tsx | 29 +-- .../start_migration/upload_rules_panels.tsx | 8 +- 8 files changed, 18 insertions(+), 493 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx diff --git a/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts b/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts index 387e9d66865b3..444aa7831802d 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/hooks/use_url_detail.ts +++ b/x-pack/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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx deleted file mode 100644 index dfb762eec4e33..0000000000000 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx +++ /dev/null @@ -1,83 +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, - EuiLoadingSpinner, - EuiIcon, -} from '@elastic/eui'; -import { AssistantAvatar } from '@kbn/elastic-assistant'; -import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; -import * as i18n from '../translations'; -import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; -import { CardSubduedText } from '../../../common/card_subdued_text'; - -export interface MigrationProgressPanelProps { - migrationStats: RuleMigrationStats; -} -export const MigrationProgressPanel = React.memo( - ({ migrationStats }) => { - const finished = migrationStats.rules.completed + migrationStats.rules.failed; - const progressValue = (finished / migrationStats.rules.total) * 100; - - const preparing = migrationStats.rules.pending === migrationStats.rules.total; - - return ( - - - - -

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

-
-
- - - {i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION(migrationStats.rules.total)} - - - {preparing ? ( - - - - - - - {i18n.START_MIGRATION_CARD_PREPARING_DESCRIPTION} - - - - - - - ) : ( - - - - )} -
-
- ); - } -); -MigrationProgressPanel.displayName = 'MigrationProgressPanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx b/x-pack/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/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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx deleted file mode 100644 index 46422189df144..0000000000000 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx +++ /dev/null @@ -1,212 +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 moment from 'moment'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiPanel, - EuiHorizontalRule, - EuiIcon, - EuiBasicTable, - EuiBadge, - EuiHealth, -} from '@elastic/eui'; -import { Chart, BarSeries, Axis, Settings, Position, ScaleType } from '@elastic/charts'; -import { SecurityPageName } from '@kbn/security-solution-navigation'; -import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; -import { - convertTranslationResultIntoColor, - convertTranslationResultIntoText, - statusToColorMap, -} from '../../../../../../../siem_migrations/rules/utils/translation_results'; -import type { RuleMigrationTranslationStats } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; -import { useGetMigrationTranslationStats } from '../../../../../../../siem_migrations/rules/logic/use_get_migration_translation_stats'; -import { CenteredLoadingSpinner } from '../../../../../../../common/components/centered_loading_spinner'; -import { SecuritySolutionLinkButton } from '../../../../../../../common/components/links'; -import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; -import * as i18n from '../translations'; -// import '@elastic/charts/dist/theme_light.css'; -import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; -import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; - -export interface MigrationResultPanelProps { - migrationStats: RuleMigrationStats; -} -export const MigrationResultPanel = React.memo(({ migrationStats }) => { - const { data: translationStats, isLoading: isLoadingTranslationStats } = - useGetMigrationTranslationStats(migrationStats.id); - 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}

-
-
-
-
- - - - - {isLoadingTranslationStats ? ( - - ) : ( - translationStats && ( - <> - - - - ) - )} - - - - - - {i18n.VIEW_TRANSLATED_RULES_BUTTON} - - - - - - - -
-
-
- ); -}); -MigrationResultPanel.displayName = 'MigrationResultPanel'; - -const TranslationResultsChart = React.memo<{ - translationStats: RuleMigrationTranslationStats; -}>(({ translationStats }) => { - const data = [ - { category: 'Results', type: 'Full', value: translationStats.rules.success.result.full }, - { category: 'Results', type: 'Partial', value: translationStats.rules.success.result.partial }, - { - category: 'Results', - type: 'Untranslatable', - value: translationStats.rules.success.result.untranslatable, - }, - { category: 'Results', type: 'Failed', value: translationStats.rules.failed }, - ]; - - const colors = [ - statusToColorMap[RuleTranslationResult.FULL], - statusToColorMap[RuleTranslationResult.PARTIAL], - statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], - 'danger', - ]; - - return ( - - - - - ); -}); -TranslationResultsChart.displayName = 'TranslationResultsChart'; - -const TranslationResultsTable = React.memo<{ - translationStats: RuleMigrationTranslationStats; -}>(({ translationStats }) => { - const items = useMemo(() => { - return [ - { - id: 'full', - title: convertTranslationResultIntoText(RuleTranslationResult.FULL), - value: translationStats.rules.success.result.full, - color: statusToColorMap[RuleTranslationResult.FULL], - }, - { - id: 'partial', - title: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL), - value: translationStats.rules.success.result.partial, - color: statusToColorMap[RuleTranslationResult.PARTIAL], - }, - { - id: 'untranslatable', - title: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE), - value: translationStats.rules.success.result.untranslatable, - color: statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], - }, - { - id: 'failed', - title: 'Failed', - value: translationStats.rules.failed, - color: 'danger', - }, - ]; - }, [translationStats]); - - return ( - {value}, - }, - { - field: 'value', - name: 'Rules', - align: 'right', - }, - ]} - /> - ); -}); -TranslationResultsTable.displayName = 'TranslationResultsTable'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts deleted file mode 100644 index 0aef40dfeb442..0000000000000 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { css } from '@emotion/css'; - -export const useStyles = (compressed: boolean) => { - const logoSize = compressed ? '32px' : '88px'; - return css` - .siemMigrationsIcon { - width: ${logoSize}; - block-size: ${logoSize}; - inline-size: ${logoSize}; - } - `; -}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx deleted file mode 100644 index edcff3646c5aa..0000000000000 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx +++ /dev/null @@ -1,80 +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, - EuiIcon, - EuiButton, - 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 { useStyles } from './upload_rules_panel.styles'; - -export interface UploadRulesPanelProps { - isUploadMore?: boolean; -} -export const UploadRulesPanel = React.memo(({ isUploadMore = false }) => { - const styles = useStyles(isUploadMore); - const { openFlyout } = useStartMigrationContext(); - const onOpenFlyout = useCallback(() => { - openFlyout(); - }, [openFlyout]); - - return ( - - - - - - - {isUploadMore ? ( - -

{i18n.START_MIGRATION_CARD_UPLOAD_MORE_TITLE}

-
- ) : ( - - - -

{i18n.START_MIGRATION_CARD_UPLOAD_TITLE}

-
-
- - -

{i18n.START_MIGRATION_CARD_UPLOAD_DESCRIPTION}

-
-
- - -

{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}

-
-
-
- )} -
- - {isUploadMore ? ( - - {i18n.START_MIGRATION_CARD_UPLOAD_MORE_BUTTON} - - ) : ( - - {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} - - )} - -
-
- ); -}); -UploadRulesPanel.displayName = 'UploadRulesPanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index a8d7aa78d0c93..3971ff4682e39 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -5,18 +5,16 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { EuiSpacer, EuiText } from '@elastic/eui'; +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 { useStyles } from './start_migration_card.styles'; import * as i18n from './translations'; import { MissingAIConnectorCallout } from './missing_ai_connector_callout'; @@ -26,11 +24,6 @@ export const StartMigrationCard: OnboardingCardComponent = React.memo( 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,17 +33,6 @@ 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); - }, []); - if (!isCardComplete(OnboardingCardId.siemMigrationsAiConnectors)) { return ( + {isLoading ? ( @@ -74,10 +56,7 @@ export const StartMigrationCard: OnboardingCardComponent = React.memo(

{i18n.START_MIGRATION_CARD_FOOTER_NOTE}

- {isFlyoutOpen && ( - - )} - +
); } ); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx index 6d011fc5fbb5b..2f5cc70307a48 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx @@ -9,10 +9,10 @@ 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'; +import { UploadRulesPanel } from '../../../../../../siem_migrations/rules/components/panels/upload_rules_panel'; +import { MigrationProgressPanel } from '../../../../../../siem_migrations/rules/components/panels/migration_progress_panel'; +import { MigrationResultPanel } from '../../../../../../siem_migrations/rules/components/panels/migration_result_panel'; +import { MigrationReadyPanel } from '../../../../../../siem_migrations/rules/components/panels/migration_ready_panel'; export interface UploadRulesPanelsProps { migrationsStats: RuleMigrationStats[]; From b3bb5ebc305ebec488c313b7b1c72b7f8b94268e Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 16 Dec 2024 12:33:48 +0100 Subject: [PATCH 39/48] move code to siem_migrations public directory --- .../components/data_input_flyout/context.tsx | 43 ++++ .../data_input_flyout/data_input_wrapper.tsx | 45 ++++ .../panels/migration_progress_panel.tsx | 76 +++++++ .../panels/migration_ready_panel.tsx | 68 ++++++ .../panels/migration_result_panel.tsx | 212 ++++++++++++++++++ .../panels/upload_rules_panel.styles.ts | 19 ++ .../components/panels/upload_rules_panel.tsx | 80 +++++++ 7 files changed, 543 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/context.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_wrapper.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_progress_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_ready_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_result_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.styles.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.tsx diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/context.tsx b/x-pack/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/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_wrapper.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_wrapper.tsx new file mode 100644 index 0000000000000..e1adf10816006 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_wrapper.tsx @@ -0,0 +1,45 @@ +/* + * 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 { PropsWithChildren } from 'react'; +import React, { useCallback, useState } from 'react'; +import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { RuleMigrationDataInputContextProvider } from './context'; +import { MigrationDataInputFlyout } from './data_input_flyout'; + +interface RuleMigrationDataInputWrapperProps { + onFlyoutClosed: () => 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/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_progress_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_progress_panel.tsx new file mode 100644 index 0000000000000..a173cf20929eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_progress_panel.tsx @@ -0,0 +1,76 @@ +/* + * 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, +} from '@elastic/eui'; +import { AssistantIcon } from '@kbn/ai-assistant-icon'; +import type { RuleMigrationStats } from '../../types'; +import * as i18n from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations'; +import { TITLE_CLASS_NAME } from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles'; +import { CardSubduedText } from '../../../../onboarding/components/onboarding_body/cards/common/card_subdued_text'; + +export interface MigrationProgressPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationProgressPanel = React.memo( + ({ migrationStats }) => { + const finished = migrationStats.rules.completed + migrationStats.rules.failed; + const progressValue = (finished / migrationStats.rules.total) * 100; + + const preparing = migrationStats.rules.pending === migrationStats.rules.total; + + return ( + + + + +

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

+
+
+ + + {i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION(migrationStats.rules.total)} + + + {preparing ? ( + + + + + + + {i18n.START_MIGRATION_CARD_PREPARING_DESCRIPTION} + + + + + + + ) : ( + + + + )} +
+
+ ); + } +); +MigrationProgressPanel.displayName = 'MigrationProgressPanel'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_ready_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_ready_panel.tsx new file mode 100644 index 0000000000000..2045399b47fc1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_ready_panel.tsx @@ -0,0 +1,68 @@ +/* + * 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 '../../service/hooks/use_start_migration'; +import type { RuleMigrationStats } from '../../types'; +import * as i18n from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations'; +import { TITLE_CLASS_NAME } from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles'; +import { useRuleMigrationDataInputContext } from '../data_input_flyout/context'; + +export interface MigrationReadyPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationReadyPanel = React.memo(({ migrationStats }) => { + const { openFlyout } = useRuleMigrationDataInputContext(); + 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/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_result_panel.tsx new file mode 100644 index 0000000000000..4cd5e23d4ceaf --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_result_panel.tsx @@ -0,0 +1,212 @@ +/* + * 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, + EuiText, + EuiPanel, + EuiHorizontalRule, + EuiIcon, + EuiBasicTable, + EuiBadge, + EuiHealth, +} from '@elastic/eui'; +import { Chart, BarSeries, Axis, Settings, Position, ScaleType } from '@elastic/charts'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; +import { + convertTranslationResultIntoColor, + convertTranslationResultIntoText, + statusToColorMap, +} 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 * as i18n from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations'; +// import '@elastic/charts/dist/theme_light.css'; +import { TITLE_CLASS_NAME } from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles'; +import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; + +export interface MigrationResultPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationResultPanel = React.memo(({ migrationStats }) => { + const { data: translationStats, isLoading: isLoadingTranslationStats } = + useGetMigrationTranslationStats(migrationStats.id); + 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}

+
+
+
+
+ + + + + {isLoadingTranslationStats ? ( + + ) : ( + translationStats && ( + <> + + + + ) + )} + + + + + + {i18n.VIEW_TRANSLATED_RULES_BUTTON} + + + + + + + +
+
+
+ ); +}); +MigrationResultPanel.displayName = 'MigrationResultPanel'; + +const TranslationResultsChart = React.memo<{ + translationStats: RuleMigrationTranslationStats; +}>(({ translationStats }) => { + const data = [ + { category: 'Results', type: 'Full', value: translationStats.rules.success.result.full }, + { category: 'Results', type: 'Partial', value: translationStats.rules.success.result.partial }, + { + category: 'Results', + type: 'Untranslatable', + value: translationStats.rules.success.result.untranslatable, + }, + { category: 'Results', type: 'Failed', value: translationStats.rules.failed }, + ]; + + const colors = [ + statusToColorMap[RuleTranslationResult.FULL], + statusToColorMap[RuleTranslationResult.PARTIAL], + statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], + 'danger', + ]; + + return ( + + + + + ); +}); +TranslationResultsChart.displayName = 'TranslationResultsChart'; + +const TranslationResultsTable = React.memo<{ + translationStats: RuleMigrationTranslationStats; +}>(({ translationStats }) => { + const items = useMemo(() => { + return [ + { + id: 'full', + title: convertTranslationResultIntoText(RuleTranslationResult.FULL), + value: translationStats.rules.success.result.full, + color: statusToColorMap[RuleTranslationResult.FULL], + }, + { + id: 'partial', + title: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL), + value: translationStats.rules.success.result.partial, + color: statusToColorMap[RuleTranslationResult.PARTIAL], + }, + { + id: 'untranslatable', + title: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE), + value: translationStats.rules.success.result.untranslatable, + color: statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], + }, + { + id: 'failed', + title: 'Failed', + value: translationStats.rules.failed, + color: 'danger', + }, + ]; + }, [translationStats]); + + return ( + {value}, + }, + { + field: 'value', + name: 'Rules', + align: 'right', + }, + ]} + /> + ); +}); +TranslationResultsTable.displayName = 'TranslationResultsTable'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.styles.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.styles.ts new file mode 100644 index 0000000000000..0aef40dfeb442 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.styles.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 { css } from '@emotion/css'; + +export const useStyles = (compressed: boolean) => { + const logoSize = compressed ? '32px' : '88px'; + return css` + .siemMigrationsIcon { + width: ${logoSize}; + block-size: ${logoSize}; + inline-size: ${logoSize}; + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.tsx new file mode 100644 index 0000000000000..d6c7fb96ea548 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiIcon, + EuiButton, + EuiButtonEmpty, + EuiPanel, +} from '@elastic/eui'; +import { SiemMigrationsIcon } from '../../../common/icon'; +import * as i18n from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations'; +import { TITLE_CLASS_NAME } from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles'; +import { useRuleMigrationDataInputContext } from '../data_input_flyout/context'; +import { useStyles } from './upload_rules_panel.styles'; + +export interface UploadRulesPanelProps { + isUploadMore?: boolean; +} +export const UploadRulesPanel = React.memo(({ isUploadMore = false }) => { + const styles = useStyles(isUploadMore); + const { openFlyout } = useRuleMigrationDataInputContext(); + const onOpenFlyout = useCallback(() => { + openFlyout(); + }, [openFlyout]); + + return ( + + + + + + + {isUploadMore ? ( + +

{i18n.START_MIGRATION_CARD_UPLOAD_MORE_TITLE}

+
+ ) : ( + + + +

{i18n.START_MIGRATION_CARD_UPLOAD_TITLE}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_DESCRIPTION}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}

+
+
+
+ )} +
+ + {isUploadMore ? ( + + {i18n.START_MIGRATION_CARD_UPLOAD_MORE_BUTTON} + + ) : ( + + {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} + + )} + +
+
+ ); +}); +UploadRulesPanel.displayName = 'UploadRulesPanel'; From 67680b220035c7c06779cba9a3fab5cba3fab638 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 16 Dec 2024 16:48:52 +0100 Subject: [PATCH 40/48] centralize panels components --- .../common/components/panel_text/index.ts | 8 ++ .../components/panel_text/panel_text.tsx | 34 ++++++++ .../start_migration/context.tsx | 35 -------- .../rule_migrations_panels.tsx | 59 ++++++++++++++ .../start_migration/start_migration_card.tsx | 35 ++++---- .../start_migration/translations.ts | 71 ---------------- .../upload_rules_panel.styles.ts | 0 .../start_migration}/upload_rules_panel.tsx | 13 ++- .../start_migration/upload_rules_panels.tsx | 46 ----------- .../migration_progress_panel.tsx | 46 ++++++----- .../migration_ready_panel.tsx | 29 +++---- .../migration_result_panel.tsx | 56 +++++++------ .../migration_status_panels/read_more.tsx | 37 +++++++++ .../migration_status_panels/translations.ts | 80 +++++++++++++++++++ .../upload_missing.tsx | 37 +++++++++ .../rules/utils/translation_results/index.ts | 2 +- 16 files changed, 353 insertions(+), 235 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/panel_text/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/panel_text/panel_text.tsx delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx rename x-pack/plugins/security_solution/public/{siem_migrations/rules/components/panels => onboarding/components/onboarding_body/cards/siem_migrations/start_migration}/upload_rules_panel.styles.ts (100%) rename x-pack/plugins/security_solution/public/{siem_migrations/rules/components/panels => onboarding/components/onboarding_body/cards/siem_migrations/start_migration}/upload_rules_panel.tsx (82%) delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx rename x-pack/plugins/security_solution/public/siem_migrations/rules/components/{panels => migration_status_panels}/migration_progress_panel.tsx (59%) rename x-pack/plugins/security_solution/public/siem_migrations/rules/components/{panels => migration_status_panels}/migration_ready_panel.tsx (69%) rename x-pack/plugins/security_solution/public/siem_migrations/rules/components/{panels => migration_status_panels}/migration_result_panel.tsx (81%) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/panel_text/index.ts b/x-pack/plugins/security_solution/public/common/components/panel_text/index.ts new file mode 100644 index 0000000000000..e11d5f18e46be --- /dev/null +++ b/x-pack/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/plugins/security_solution/public/common/components/panel_text/panel_text.tsx b/x-pack/plugins/security_solution/public/common/components/panel_text/panel_text.tsx new file mode 100644 index 0000000000000..c4f8b1c45670f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/panel_text/panel_text.tsx @@ -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 React, { type PropsWithChildren } from 'react'; +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: React.CSSProperties = {}; + if (semiBold) { + style.fontWeight = euiTheme.font.weight.semiBold; + } + + return ( + + {children} + + ); +}); +PanelText.displayName = 'PanelText'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx b/x-pack/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/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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx b/x-pack/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/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/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index 3971ff4682e39..baebbde53b4cf 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useEffect } 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'; @@ -14,10 +15,9 @@ import { useLatestStats } from '../../../../../../siem_migrations/rules/service/ 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 { 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 }) => { @@ -33,15 +33,14 @@ export const StartMigrationCard: OnboardingCardComponent = React.memo( } }, [isCardComplete, migrationsStats, setComplete]); - if (!isCardComplete(OnboardingCardId.siemMigrationsAiConnectors)) { - return ( - - setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors) - } - /> - ); - } + const isConnectorsCardComplete = useMemo( + () => isCardComplete(OnboardingCardId.siemMigrationsAiConnectors), + [isCardComplete] + ); + + const expandConnectorsCard = useCallback(() => { + setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors); + }, [setExpandedCardId]); return ( @@ -49,12 +48,16 @@ export const StartMigrationCard: OnboardingCardComponent = React.memo( {isLoading ? ( ) : ( - + )} - +

{i18n.START_MIGRATION_CARD_FOOTER_NOTE}

-
+
); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index 58146cbfa2271..4073423f1f8ae 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -55,74 +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 = (totalRules: number) => - i18n.translate('xpack.securitySolution.onboarding.startMigration.progress.description', { - defaultMessage: `Processing migration of {totalRules} rules.`, - values: { totalRules }, - }); -export const START_MIGRATION_CARD_PREPARING_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.preparing.description', - { - defaultMessage: `Preparing environment for the AI powered migration.`, - } -); -export const START_MIGRATION_CARD_PROCESSING = (rulesLeft: number) => - i18n.translate('xpack.securitySolution.onboarding.startMigration.processing', { - defaultMessage: `{rulesLeft} rules left...`, - values: { rulesLeft }, - }); - -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/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.styles.ts b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.styles.ts rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.styles.ts diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.tsx rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx index d6c7fb96ea548..1a9bd2d17b945 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/upload_rules_panel.tsx +++ b/x-pack/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 '../../../common/icon'; -import * as i18n from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations'; -import { TITLE_CLASS_NAME } from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles'; -import { useRuleMigrationDataInputContext } from '../data_input_flyout/context'; +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 { @@ -55,9 +56,7 @@ export const UploadRulesPanel = React.memo(({ isUploadMor - -

{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}

-
+
)} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx deleted file mode 100644 index 2f5cc70307a48..0000000000000 --- a/x-pack/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 '../../../../../../siem_migrations/rules/components/panels/upload_rules_panel'; -import { MigrationProgressPanel } from '../../../../../../siem_migrations/rules/components/panels/migration_progress_panel'; -import { MigrationResultPanel } from '../../../../../../siem_migrations/rules/components/panels/migration_result_panel'; -import { MigrationReadyPanel } from '../../../../../../siem_migrations/rules/components/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/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_progress_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_progress_panel.tsx rename to x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx index a173cf20929eb..0be6fa7b75f5a 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_progress_panel.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx @@ -14,20 +14,21 @@ import { 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 '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations'; -import { TITLE_CLASS_NAME } from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles'; -import { CardSubduedText } from '../../../../onboarding/components/onboarding_body/cards/common/card_subdued_text'; +import * as i18n from './translations'; +import { RuleMigrationsReadMore } from './read_more'; export interface MigrationProgressPanelProps { migrationStats: RuleMigrationStats; } export const MigrationProgressPanel = React.memo( ({ migrationStats }) => { - const finished = migrationStats.rules.completed + migrationStats.rules.failed; - const progressValue = (finished / migrationStats.rules.total) * 100; + const finishedCount = migrationStats.rules.completed + migrationStats.rules.failed; + const progressValue = (finishedCount / migrationStats.rules.total) * 100; const preparing = migrationStats.rules.pending === migrationStats.rules.total; @@ -35,16 +36,17 @@ export const MigrationProgressPanel = React.memo( - -

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

-
+ +

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

+
- {i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION(migrationStats.rules.total)} + {i18n.RULE_MIGRATION_PROGRESS_DESCRIPTION(migrationStats.rules.total)} - {preparing ? ( + + ( - - {i18n.START_MIGRATION_CARD_PREPARING_DESCRIPTION} - + + {preparing ? i18n.RULE_MIGRATION_PREPARING : i18n.RULE_MIGRATION_TRANSLATING} +
- ) : ( - - - - )} + {!preparing && ( + <> + + + + + )} +
); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_ready_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx similarity index 69% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_ready_panel.tsx rename to x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx index 2045399b47fc1..090ca9b54e53b 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_ready_panel.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx @@ -6,19 +6,12 @@ */ import React, { useCallback } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiButton, - EuiButtonEmpty, - EuiPanel, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiPanel } from '@elastic/eui'; +import { PanelText } from '../../../../common/components/panel_text'; import { useStartMigration } from '../../service/hooks/use_start_migration'; import type { RuleMigrationStats } from '../../types'; -import * as i18n from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations'; -import { TITLE_CLASS_NAME } from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles'; import { useRuleMigrationDataInputContext } from '../data_input_flyout/context'; +import * as i18n from './translations'; export interface MigrationReadyPanelProps { migrationStats: RuleMigrationStats; @@ -40,25 +33,25 @@ export const MigrationReadyPanel = React.memo(({ migra - -

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

-
+ +

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

+
- -

{i18n.START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION}

-
+ +

{i18n.RULE_MIGRATION_READY_DESCRIPTION}

+
- {i18n.START_MIGRATION_CARD_TRANSLATE_BUTTON} + {i18n.RULE_MIGRATION_START_TRANSLATION_BUTTON} - {i18n.START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON} + {i18n.RULE_MIGRATION_UPLOAD_MACROS_BUTTON} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_result_panel.tsx rename to x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx index a7b07c9a6446d..5503de614cbdf 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/panels/migration_result_panel.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx @@ -10,7 +10,6 @@ import moment from 'moment'; import { EuiFlexGroup, EuiFlexItem, - EuiText, EuiPanel, EuiHorizontalRule, EuiIcon, @@ -19,7 +18,8 @@ import { } from '@elastic/eui'; import { Chart, BarSeries, Settings, ScaleType } from '@elastic/charts'; import { SecurityPageName } from '@kbn/security-solution-navigation'; -import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; +import { AssistantIcon } from '@kbn/ai-assistant-icon'; +import { PanelText } from '../../../../common/components/panel_text'; import { convertTranslationResultIntoText, statusToColorMap, @@ -29,10 +29,8 @@ import { useGetMigrationTranslationStats } from '../../logic/use_get_migration_t import { CenteredLoadingSpinner } from '../../../../common/components/centered_loading_spinner'; import { SecuritySolutionLinkButton } from '../../../../common/components/links'; import type { RuleMigrationStats } from '../../types'; -import * as i18n from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations'; -// import '@elastic/charts/dist/theme_light.css'; -import { TITLE_CLASS_NAME } from '../../../../onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles'; import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; +import * as i18n from './translations'; export interface MigrationResultPanelProps { migrationStats: RuleMigrationStats; @@ -45,19 +43,19 @@ export const MigrationResultPanel = React.memo(({ mig - -

{i18n.START_MIGRATION_CARD_RESULT_TITLE(migrationStats.number)}

-
+ +

{i18n.RULE_MIGRATION_COMPLETE_TITLE(migrationStats.number)}

+
- +

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

-
+
@@ -67,12 +65,12 @@ export const MigrationResultPanel = React.memo(({ mig - + - -

{i18n.VIEW_TRANSLATED_RULES_TITLE}

-
+ +

{i18n.RULE_MIGRATION_SUMMARY_TITLE}

+
@@ -98,7 +96,7 @@ export const MigrationResultPanel = React.memo(({ mig deepLinkId={SecurityPageName.siemMigrationsRules} path={migrationStats.id} > - {i18n.VIEW_TRANSLATED_RULES_BUTTON} + {i18n.RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON} @@ -117,14 +115,26 @@ const TranslationResultsChart = React.memo<{ translationStats: RuleMigrationTranslationStats; }>(({ translationStats }) => { const data = [ - { category: 'Results', type: 'Full', value: translationStats.rules.success.result.full }, - { category: 'Results', type: 'Partial', value: translationStats.rules.success.result.partial }, { category: 'Results', - type: 'Untranslatable', + 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: 'Failed', value: translationStats.rules.failed }, + { + category: 'Results', + type: i18n.RULE_MIGRATION_TRANSLATION_FAILED, + value: translationStats.rules.failed, + }, ]; const colors = [ @@ -179,7 +189,7 @@ const TranslationResultsTable = React.memo<{ }, { id: 'failed', - title: 'Failed', + title: i18n.RULE_MIGRATION_TRANSLATION_FAILED, value: translationStats.rules.failed, color: 'danger', }, @@ -195,12 +205,12 @@ const TranslationResultsTable = React.memo<{ columns={[ { field: 'title', - name: 'Result', + name: i18n.RULE_MIGRATION_TABLE_COLUMN_RESULT, render: (value: string, { color }) => {value}, }, { field: 'value', - name: 'Rules', + name: i18n.RULE_MIGRATION_TABLE_COLUMN_RULES, align: 'right', }, ]} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx b/x-pack/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/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/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts new file mode 100644 index 0000000000000..b0a237accd55b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULE_MIGRATION_READY_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.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 RULE_MIGRATION_START_TRANSLATION_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.translate.button', + { defaultMessage: 'Start translation' } +); +export const RULE_MIGRATION_UPLOAD_MACROS_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.uploadMacros.button', + { defaultMessage: 'Upload macros' } +); + +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_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' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing.tsx new file mode 100644 index 0000000000000..7cf14b30d6c0a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing.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 RuleMigrationsUploadMissing = React.memo(() => { + const docLink = useKibana().services.docLinks.links.siem.gettingStarted; + return ( + +

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

+
+ ); +}); +RuleMigrationsUploadMissing.displayName = 'RuleMigrationsUploadMissing'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts index 57fa9f3327166..250702c9bb002 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts @@ -45,6 +45,6 @@ export const convertTranslationResultIntoText = (status?: RuleMigrationTranslati return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL; default: - throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status)); + return i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL; } }; From 27637bdc57437f154d2ece22ff41ccdd171798a0 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:04:24 +0000 Subject: [PATCH 41/48] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/security_solution/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 7767fffe69824..372768e094f56 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/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" ] } From 876ddda0d921a06b681bcac3ec3d1994496e34f8 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 16 Dec 2024 20:30:23 +0100 Subject: [PATCH 42/48] fix macros resource check --- config/serverless.security.yml | 1 - .../data_input_flyout/data_input_flyout.tsx | 8 +- .../steps/lookups/lookups_data_input.tsx | 12 +-- .../sub_steps/missing_lookups_list/index.tsx | 17 +++- .../missing_lookups_list.tsx | 21 +++-- .../steps/macros/macros_data_input.tsx | 15 +--- .../migration_ready_panel.tsx | 86 ++++++++++++------ .../migration_result_panel.tsx | 14 +-- .../migration_status_panels/translations.ts | 38 +++++--- .../upload_missing.tsx | 37 -------- .../upload_missing_panel.tsx | 89 +++++++++++++++++++ .../data/rule_migrations_data_rules_client.ts | 2 +- 12 files changed, 221 insertions(+), 119 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx diff --git a/config/serverless.security.yml b/config/serverless.security.yml index 895c9399ef624..47a67c293565a 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -7,7 +7,6 @@ xpack.infra.enabled: false xpack.observabilityLogsExplorer.enabled: false xpack.observability.enabled: false xpack.observabilityAIAssistant.enabled: false -xpack.inventory.enabled: false xpack.search.notebooks.enabled: false xpack.searchPlayground.enabled: false xpack.searchInferenceEndpoints.enabled: false diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index 58a9872ed859b..9062e3a6b21e8 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -86,10 +86,7 @@ export const MigrationDataInputFlyout = React.memo { - setDataInputStep(DataInputStep.Lookups); - }, []); - const onLookupsCreated = useCallback(() => { + const onAllLookupsCreated = useCallback(() => { setDataInputStep(DataInputStep.End); }, []); @@ -126,7 +123,6 @@ export const MigrationDataInputFlyout = React.memo @@ -135,7 +131,7 @@ export const MigrationDataInputFlyout = React.memo diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx index 4214c4272c7c1..a8fca750ce5da 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/lookups_data_input.tsx @@ -34,7 +34,7 @@ export type AddUploadedLookups = (lookups: RuleMigrationResourceData[]) => void; interface LookupsDataInputSubStepsProps { migrationStats: RuleMigrationTaskStats; missingLookups: string[]; - onLookupsCreated: OnResourcesCreated; + onAllLookupsCreated: OnResourcesCreated; } interface LookupsDataInputProps extends Omit { @@ -43,7 +43,7 @@ interface LookupsDataInputProps missingLookups?: string[]; } export const LookupsDataInput = React.memo( - ({ dataInputStep, migrationStats, missingLookups, onLookupsCreated }) => { + ({ dataInputStep, migrationStats, missingLookups, onAllLookupsCreated }) => { const dataInputStatus = useMemo( () => getStatus(DataInputStep.Lookups, dataInputStep), [dataInputStep] @@ -79,7 +79,7 @@ export const LookupsDataInput = React.memo( @@ -94,7 +94,7 @@ LookupsDataInput.displayName = 'LookupsDataInput'; const END = 10 as const; type SubStep = 1 | 2 | typeof END; export const LookupsDataInputSubSteps = React.memo( - ({ migrationStats, missingLookups, onLookupsCreated }) => { + ({ migrationStats, missingLookups, onAllLookupsCreated }) => { const [subStep, setSubStep] = useState(1); const [uploadedLookups, setUploadedLookups] = useState({}); @@ -110,9 +110,9 @@ export const LookupsDataInputSubSteps = React.memo { if (missingLookups.every((lookupName) => uploadedLookups[lookupName])) { setSubStep(END); - onLookupsCreated(); + onAllLookupsCreated(); } - }, [uploadedLookups, missingLookups, onLookupsCreated]); + }, [uploadedLookups, missingLookups, onAllLookupsCreated]); // Copy query step const onCopied = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx index d20c997838206..ae1dbc0a03b3c 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +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'; @@ -30,7 +30,7 @@ export const useMissingLookupsListStep = ({ addUploadedLookups, onCopied, }: MissingLookupsListStepProps): EuiStepProps => { - const { upsertResources, isLoading } = useUpsertResources(addUploadedLookups); + const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups); const clearLookup = useCallback( (lookupName: string) => { @@ -41,16 +41,25 @@ export const useMissingLookupsListStep = ({ [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, + status: listStepStatus, children: ( ), }; diff --git a/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/missing_lookups_list.tsx index 9cec751e563a8..1e1b5464c0aaf 100644 --- a/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/missing_lookups_list.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { css } from '@emotion/css'; import { EuiButtonIcon, @@ -32,15 +32,14 @@ interface MissingLookupsListProps { missingLookups: string[]; uploadedLookups: UploadedLookups; clearLookup: (lookupsName: string) => void; - isLoading: boolean; onCopied: () => void; } export const MissingLookupsList = React.memo( - ({ missingLookups, uploadedLookups, clearLookup, isLoading, onCopied }) => { + ({ missingLookups, uploadedLookups, clearLookup, onCopied }) => { const { euiTheme } = useEuiTheme(); return ( <> - + {missingLookups.map((lookupName) => { return ( @@ -136,19 +135,25 @@ interface ClearLookupButtonProps { isDisabled: boolean; } const ClearLookupButton = React.memo( - ({ lookupName, clearLookup, isDisabled }) => { + ({ lookupName, clearLookup, isDisabled: isDisabledDefault }) => { + const [isDisabled, setIsDisabled] = useState(isDisabledDefault); + const onClick = useCallback(() => { + setIsDisabled(true); + clearLookup(lookupName); + }, [clearLookup, lookupName]); + const button = useMemo( () => ( clearLookup(lookupName)} + onClick={onClick} iconType="cross" color="text" - aria-label={`${i18n.CLEAR_EMPTY_LOOKUP_TOOLTIP} ${lookupName}`} + aria-label={i18n.CLEAR_EMPTY_LOOKUP_TOOLTIP} data-test-subj="lookupNameClear" isDisabled={isDisabled} /> ), - [clearLookup, isDisabled, lookupName] + [onClick, isDisabled] ); if (isDisabled) { return button; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx index f586ad21e25f4..ebbffb6d7f6d6 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/macros_data_input.tsx @@ -21,7 +21,6 @@ import { useCheckResourcesStep } from './sub_steps/check_resources'; interface MacrosDataInputSubStepsProps { migrationStats: RuleMigrationTaskStats; missingMacros: string[]; - onMacrosCreated: OnResourcesCreated; onMissingResourcesFetched: OnMissingResourcesFetched; } interface MacrosDataInputProps @@ -31,13 +30,7 @@ interface MacrosDataInputProps missingMacros?: string[]; } export const MacrosDataInput = React.memo( - ({ - dataInputStep, - migrationStats, - missingMacros, - onMacrosCreated, - onMissingResourcesFetched, - }) => { + ({ dataInputStep, migrationStats, missingMacros, onMissingResourcesFetched }) => { const dataInputStatus = useMemo( () => getStatus(DataInputStep.Macros, dataInputStep), [dataInputStep] @@ -67,7 +60,6 @@ export const MacrosDataInput = React.memo( @@ -82,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 @@ -93,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, diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx index 090ca9b54e53b..8fac512ad54d5 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx @@ -5,57 +5,87 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, 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]); + const onOpenFlyout = useCallback(() => { openFlyout(migrationStats); }, [openFlyout, migrationStats]); - const { startMigration, isLoading } = useStartMigration(); - const onStartMigration = useCallback(() => { - startMigration(migrationStats.id); - }, [migrationStats.id, startMigration]); - return ( - + - - - -

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

-
-
- - -

{i18n.RULE_MIGRATION_READY_DESCRIPTION}

-
-
-
-
- - - {i18n.RULE_MIGRATION_START_TRANSLATION_BUTTON} - + +

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

+
- - - {i18n.RULE_MIGRATION_UPLOAD_MACROS_BUTTON} - + + {isLoading ? ( + + ) : ( + + + + {i18n.RULE_MIGRATION_READY_DESCRIPTION( + missingResources.length > 0 ? i18n.RULE_MIGRATION_READY_MISSING_RESOURCES : '' + )} + + + + {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/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx index 5503de614cbdf..a72db404291b8 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx @@ -15,6 +15,7 @@ import { EuiIcon, EuiBasicTable, EuiHealth, + EuiText, } from '@elastic/eui'; import { Chart, BarSeries, Settings, ScaleType } from '@elastic/charts'; import { SecurityPageName } from '@kbn/security-solution-navigation'; @@ -30,6 +31,7 @@ import { CenteredLoadingSpinner } from '../../../../common/components/centered_l import { SecuritySolutionLinkButton } from '../../../../common/components/links'; import type { RuleMigrationStats } from '../../types'; import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; +import { RuleMigrationsUploadMissingPanel } from './upload_missing_panel'; import * as i18n from './translations'; export interface MigrationResultPanelProps { @@ -76,13 +78,16 @@ export const MigrationResultPanel = React.memo(({ mig - + {isLoadingTranslationStats ? ( ) : ( translationStats && ( <> + + {i18n.RULE_MIGRATION_SUMMARY_CHART_TITLE} + @@ -105,6 +110,7 @@ export const MigrationResultPanel = React.memo(({ mig
+
); @@ -145,7 +151,7 @@ const TranslationResultsChart = React.memo<{ ]; return ( - + { return [ { - id: 'full', title: convertTranslationResultIntoText(RuleTranslationResult.FULL), value: translationStats.rules.success.result.full, color: statusToColorMap[RuleTranslationResult.FULL], }, { - id: 'partial', title: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL), value: translationStats.rules.success.result.partial, color: statusToColorMap[RuleTranslationResult.PARTIAL], }, { - id: 'untranslatable', title: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE), value: translationStats.rules.success.result.untranslatable, color: statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], }, { - id: 'failed', title: i18n.RULE_MIGRATION_TRANSLATION_FAILED, value: translationStats.rules.failed, color: 'danger', diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts index b0a237accd55b..96a506811b953 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts @@ -7,22 +7,21 @@ import { i18n } from '@kbn/i18n'; -export const RULE_MIGRATION_READY_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.panel.ready.description', - { +export const RULE_MIGRATION_READY_DESCRIPTION = (missingResourcesText: string) => + i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.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', - } + 'Migration is created but the translation has not started yet. {missingResourcesText}', + values: { missingResourcesText }, + }); +export const RULE_MIGRATION_READY_MISSING_RESOURCES = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.ready.description', + { 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_UPLOAD_MACROS_BUTTON = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.panel.uploadMacros.button', - { defaultMessage: 'Upload macros' } -); - export const RULE_MIGRATION_TITLE = (number: number) => i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.migrationTitle', { defaultMessage: 'SIEM rules migration #{number}', @@ -60,6 +59,11 @@ export const RULE_MIGRATION_SUMMARY_TITLE = i18n.translate( { 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' } @@ -78,3 +82,17 @@ 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/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing.tsx deleted file mode 100644 index 7cf14b30d6c0a..0000000000000 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing.tsx +++ /dev/null @@ -1,37 +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 { 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 RuleMigrationsUploadMissing = React.memo(() => { - const docLink = useKibana().services.docLinks.links.siem.gettingStarted; - return ( - -

- - - - ), - }} - /> -

-
- ); -}); -RuleMigrationsUploadMissing.displayName = 'RuleMigrationsUploadMissing'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx new file mode 100644 index 0000000000000..04d86cd54b999 --- /dev/null +++ b/x-pack/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]); + + 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/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index c3979ece239ff..47bcd56e6433e 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -337,7 +337,7 @@ 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: { status: { terms: { field: 'status' } }, createdAt: { min: { field: '@timestamp' } }, From 5d63c09680e23198124548568842765dc1877a5a Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 16 Dec 2024 20:56:34 +0100 Subject: [PATCH 43/48] fix duplicate missing resources requests --- .../steps/macros/sub_steps/macros_file_upload/index.tsx | 2 +- .../migration_status_panels/migration_ready_panel.tsx | 5 +++-- .../components/migration_status_panels/translations.ts | 9 ++++++--- .../migration_status_panels/upload_missing_panel.tsx | 2 +- .../rules/service/rule_migrations_service.ts | 4 ++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx b/x-pack/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/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/index.tsx +++ b/x-pack/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: ( (({ migra useEffect(() => { getMissingResources(migrationStats.id); - }, [getMissingResources, migrationStats]); + }, [getMissingResources, migrationStats.id]); const onOpenFlyout = useCallback(() => { openFlyout(migrationStats); @@ -48,6 +48,7 @@ export const MigrationReadyPanel = React.memo(({ migra {i18n.RULE_MIGRATION_READY_DESCRIPTION( + migrationStats.rules.total, missingResources.length > 0 ? i18n.RULE_MIGRATION_READY_MISSING_RESOURCES : '' )} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts index 96a506811b953..7dfeb4203ccc9 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts @@ -7,11 +7,14 @@ import { i18n } from '@kbn/i18n'; -export const RULE_MIGRATION_READY_DESCRIPTION = (missingResourcesText: string) => +export const RULE_MIGRATION_READY_DESCRIPTION = ( + totalRules: number, + missingResourcesText: string +) => i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.ready.description', { defaultMessage: - 'Migration is created but the translation has not started yet. {missingResourcesText}', - values: { missingResourcesText }, + '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.description', diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx index 04d86cd54b999..f1c6bdd71613a 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx @@ -36,7 +36,7 @@ export const RuleMigrationsUploadMissingPanel = React.memo { getMissingResources(migrationStats.id); - }, [getMissingResources, migrationStats]); + }, [getMissingResources, migrationStats.id]); const onOpenFlyout = useCallback(() => { openFlyout(migrationStats); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 75b7887db6525..83ead556b09cc 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/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); } } From 823d5fba24c57b6f3841659461b26bfa672d2608 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 16 Dec 2024 21:38:17 +0100 Subject: [PATCH 44/48] fix translations --- .../lookups/sub_steps/missing_lookups_list/translations.ts | 6 +++--- .../components/migration_status_panels/translations.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts index 4c0a4dc36a7eb..123e541182068 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/missing_lookups_list/translations.ts @@ -13,7 +13,7 @@ export const LOOKUPS_DATA_INPUT_COPY_TITLE = i18n.translate( ); export const MISSING_LOOKUPS_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.description', + '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.', @@ -21,10 +21,10 @@ export const MISSING_LOOKUPS_DESCRIPTION = i18n.translate( ); export const COPY_LOOKUP_NAME_TOOLTIP = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.copyLookupNameTooltip', + '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.clearEmptyLookupTooltip', + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.clearEmptyLookupTooltip', { defaultMessage: 'Mark the lookup as empty' } ); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts index 7dfeb4203ccc9..55e73bca32b5d 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts @@ -17,7 +17,7 @@ export const RULE_MIGRATION_READY_DESCRIPTION = ( values: { totalRules, missingResourcesText }, }); export const RULE_MIGRATION_READY_MISSING_RESOURCES = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.panel.ready.description', + 'xpack.securitySolution.siemMigrations.rules.panel.ready.missingResources', { defaultMessage: 'Upload macros & lookups and start the translation process' } ); From 6e3b4e23dea4bbf3d0fa18476d02df764447564f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 17 Dec 2024 13:12:05 +0100 Subject: [PATCH 45/48] theme aware vis colors --- .../migration_result_panel.tsx | 25 ++++++++++--------- .../rules/components/status_badge/index.tsx | 8 +++--- .../rules/utils/translation_results/index.ts | 23 ++++++++--------- 3 files changed, 28 insertions(+), 28 deletions(-) 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 index a72db404291b8..328263c253c2d 100644 --- 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 @@ -23,7 +23,7 @@ import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { PanelText } from '../../../../common/components/panel_text'; import { convertTranslationResultIntoText, - statusToColorMap, + 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'; @@ -31,7 +31,6 @@ import { CenteredLoadingSpinner } from '../../../../common/components/centered_l import { SecuritySolutionLinkButton } from '../../../../common/components/links'; import type { RuleMigrationStats } from '../../types'; import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; -import { RuleMigrationsUploadMissingPanel } from './upload_missing_panel'; import * as i18n from './translations'; export interface MigrationResultPanelProps { @@ -110,7 +109,7 @@ export const MigrationResultPanel = React.memo(({ mig - + {/* TODO: uncomment when retry API is ready */} ); @@ -120,6 +119,7 @@ MigrationResultPanel.displayName = 'MigrationResultPanel'; const TranslationResultsChart = React.memo<{ translationStats: RuleMigrationTranslationStats; }>(({ translationStats }) => { + const translationResultColors = useResultVisColors(); const data = [ { category: 'Results', @@ -144,10 +144,10 @@ const TranslationResultsChart = React.memo<{ ]; const colors = [ - statusToColorMap[RuleTranslationResult.FULL], - statusToColorMap[RuleTranslationResult.PARTIAL], - statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], - 'danger', + translationResultColors[RuleTranslationResult.FULL], + translationResultColors[RuleTranslationResult.PARTIAL], + translationResultColors[RuleTranslationResult.UNTRANSLATABLE], + translationResultColors.error, ]; return ( @@ -173,30 +173,31 @@ 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: statusToColorMap[RuleTranslationResult.FULL], + color: translationResultColors[RuleTranslationResult.FULL], }, { title: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL), value: translationStats.rules.success.result.partial, - color: statusToColorMap[RuleTranslationResult.PARTIAL], + color: translationResultColors[RuleTranslationResult.PARTIAL], }, { title: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE), value: translationStats.rules.success.result.untranslatable, - color: statusToColorMap[RuleTranslationResult.UNTRANSLATABLE], + color: translationResultColors[RuleTranslationResult.UNTRANSLATABLE], }, { title: i18n.RULE_MIGRATION_TRANSLATION_FAILED, value: translationStats.rules.failed, - color: 'danger', + color: translationResultColors.error, }, ]; - }, [translationStats]); + }, [translationStats, translationResultColors]); return ( = React.memo( ({ migrationRule, 'data-test-subj': dataTestSubj = 'translation-result' }) => { + const colors = useResultVisColors(); // Installed if (migrationRule.elastic_rule?.id) { return ( - + {i18n.RULE_STATUS_INSTALLED} @@ -61,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/utils/translation_results/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts index 250702c9bb002..d25c252fb8fec 100644 --- 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 @@ -5,31 +5,31 @@ * 2.0. */ -import { euiLightVars } from '@kbn/ui-theme'; +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'; -const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars; -export const statusToColorMap: Record = { - [RuleTranslationResult.FULL]: euiColorVis0, - [RuleTranslationResult.PARTIAL]: euiColorVis7, - [RuleTranslationResult.UNTRANSLATABLE]: euiColorVis9, +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: - throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status)); + return 'subdued'; } }; @@ -37,13 +37,10 @@ export const convertTranslationResultIntoText = (status?: RuleMigrationTranslati 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; } From 4a50aa9b8d5c71fa142636187f19ec424dce6dfc Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 17 Dec 2024 13:26:56 +0100 Subject: [PATCH 46/48] vis dark mode --- .../migration_result_panel.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 index 328263c253c2d..cce11abcd8eb7 100644 --- 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 @@ -16,8 +16,9 @@ import { EuiBasicTable, EuiHealth, EuiText, + useEuiTheme, } from '@elastic/eui'; -import { Chart, BarSeries, Settings, ScaleType } from '@elastic/charts'; +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'; @@ -119,6 +120,7 @@ MigrationResultPanel.displayName = 'MigrationResultPanel'; const TranslationResultsChart = React.memo<{ translationStats: RuleMigrationTranslationStats; }>(({ translationStats }) => { + const { colorMode } = useEuiTheme(); const translationResultColors = useResultVisColors(); const data = [ { @@ -152,7 +154,11 @@ const TranslationResultsChart = React.memo<{ return ( - + Date: Tue, 17 Dec 2024 13:59:24 +0100 Subject: [PATCH 47/48] address PR comments --- .../components/panel_text/panel_text.tsx | 5 +- .../lookups_file_upload.tsx | 17 +++-- .../migration_ready_panel.tsx | 64 +++++++++---------- 3 files changed, 45 insertions(+), 41 deletions(-) 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 index c4f8b1c45670f..5c1fc6746bcd0 100644 --- 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 @@ -5,6 +5,7 @@ * 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 { @@ -20,13 +21,13 @@ export const PanelText = React.memo(({ children, subdued, semiBo color = 'subdued'; } - const style: React.CSSProperties = {}; + const style: CSSInterpolation = {}; if (semiBold) { style.fontWeight = euiTheme.font.weight.semiBold; } return ( - + {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 index 048d1e38ed09d..6ea9562f24cce 100644 --- 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiButton, EuiFilePicker, @@ -14,6 +14,10 @@ import { 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'; @@ -26,8 +30,10 @@ export interface LookupsFileUploadProps { 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]); @@ -39,13 +45,13 @@ export const LookupsFileUpload = React.memo( const parseFile = useCallback( async (files: FileList | null) => { - if (!files) { - return; - } - setErrors([]); setLookupResources([]); + if (!files?.length) { + return; + } + const lookups = await Promise.all( Array.from(files).map((file) => { return new Promise((resolve) => { @@ -120,6 +126,7 @@ export const LookupsFileUpload = React.memo( > >} fullWidth initialPromptText={ <> 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 index 59fba8b117168..3c230cba4c34f 100644 --- 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 @@ -34,43 +34,39 @@ export const MigrationReadyPanel = React.memo(({ migra return ( - + - -

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

-
-
- - {isLoading ? ( - - ) : ( - - - - {i18n.RULE_MIGRATION_READY_DESCRIPTION( - migrationStats.rules.total, - missingResources.length > 0 ? i18n.RULE_MIGRATION_READY_MISSING_RESOURCES : '' - )} - - - - {missingResources.length > 0 ? ( - - {i18n.RULE_MIGRATION_UPLOAD_BUTTON} - - ) : ( - + + + +

{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} + + ) : ( + + )} + + )}
); From 4aaa5752f547c15a739266994da57d803abba03d Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 17 Dec 2024 14:02:19 +0100 Subject: [PATCH 48/48] allow mark as empty when provided --- .../missing_lookups_list/missing_lookups_list.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 index 1e1b5464c0aaf..cd462a41bb6c1 100644 --- 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 @@ -42,6 +42,7 @@ export const MissingLookupsList = React.memo( {missingLookups.map((lookupName) => { + const isMarkedAsEmpty = uploadedLookups[lookupName] === EMPTY_RESOURCE_PLACEHOLDER; return ( ( {lookupName} @@ -84,7 +81,7 @@ export const MissingLookupsList = React.memo(