diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index fa315e3c421aa..774935bd95719 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -367,6 +367,7 @@ import type { GetRuleMigrationRequestQueryInput, GetRuleMigrationRequestParamsInput, GetRuleMigrationResponse, + GetRuleMigrationIntegrationsResponse, GetRuleMigrationPrebuiltRulesRequestParamsInput, GetRuleMigrationPrebuiltRulesResponse, GetRuleMigrationResourcesRequestQueryInput, @@ -1455,6 +1456,21 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Retrieves all related integrations + */ + async getRuleMigrationIntegrations() { + this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationIntegrations`); + return this.kbnClient + .request({ + path: '/internal/siem_migrations/rules/integrations', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Retrieves all available prebuilt rules (installed and installable) */ diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts index 88ed777c21d69..019766d96e78f 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts @@ -11,6 +11,8 @@ export const SIEM_MIGRATIONS_PATH = '/internal/siem_migrations' as const; export const SIEM_RULE_MIGRATIONS_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const; export const SIEM_RULE_MIGRATIONS_ALL_STATS_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/stats` as const; +export const SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH = + `${SIEM_RULE_MIGRATIONS_PATH}/integrations` as const; export const SIEM_RULE_MIGRATION_CREATE_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id?}` as const; export const SIEM_RULE_MIGRATION_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}` as const; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 47c06e1e02c7a..7a6cbec4d2d1d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -28,6 +28,7 @@ import { RuleMigrationResourceType, RuleMigrationResource, } from '../../rule_migration.gen'; +import { RelatedIntegration } from '../../../../api/detection_engine/model/rule_schema/common_attributes.gen'; import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { ConnectorId, LangSmithOptions } from '../../common.gen'; @@ -79,6 +80,14 @@ export const GetRuleMigrationResponse = z.object({ data: z.array(RuleMigration), }); +/** + * The map of related integrations, with the integration id as a key + */ +export type GetRuleMigrationIntegrationsResponse = z.infer< + typeof GetRuleMigrationIntegrationsResponse +>; +export const GetRuleMigrationIntegrationsResponse = z.object({}).catchall(RelatedIntegration); + export type GetRuleMigrationPrebuiltRulesRequestParams = z.infer< typeof GetRuleMigrationPrebuiltRulesRequestParams >; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index 69e43b57dabd3..39feb3a51897c 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -54,6 +54,26 @@ paths: items: $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTaskStats' + /internal/siem_migrations/rules/integrations: + get: + summary: Retrieves all related integrations for a specific migration + operationId: GetRuleMigrationIntegrations + x-codegen-enabled: true + x-internal: true + description: Retrieves all related integrations + tags: + - SIEM Rule Migrations + responses: + 200: + description: Indicates that related integrations have been retrieved correctly. + content: + application/json: + schema: + type: object + description: The map of related integrations, with the integration id as a key + additionalProperties: + $ref: '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml#/components/schemas/RelatedIntegration' + ## Specific rule migration APIs /internal/siem_migrations/rules/{migration_id}: diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts index 02fb423b05279..2465a465d28d7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -23,6 +23,7 @@ import { SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH, SIEM_RULE_MIGRATION_RESOURCES_PATH, SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH, + SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH, } from '../../../../common/siem_migrations/constants'; import type { CreateRuleMigrationRequestBody, @@ -39,6 +40,7 @@ import type { UpsertRuleMigrationResourcesResponse, GetRuleMigrationPrebuiltRulesResponse, UpdateRuleMigrationResponse, + GetRuleMigrationIntegrationsResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; export interface GetRuleMigrationStatsParams { @@ -279,6 +281,20 @@ export const getRuleMigrationsPrebuiltRules = async ({ ); }; +export interface GetIntegrationsParams { + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Retrieves existing integrations. */ +export const getIntegrations = async ({ + signal, +}: GetIntegrationsParams): Promise => { + return KibanaServices.get().http.get( + SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH, + { version: '1', signal } + ); +}; + export interface UpdateRulesParams { /** The list of migration rules data to update */ rulesToUpdate: UpdateRuleMigrationData[]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx index b883934a0bdcb..ba86a7587b5f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import type { RelatedIntegration, RuleResponse } from '../../../../../common/api/detection_engine'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { EmptyMigration } from './empty_migration'; @@ -43,13 +44,23 @@ export interface MigrationRulesTableProps { * Selected rule migration id */ migrationId: string; + + /** + * Existing integrations. + */ + integrations?: Record; + + /** + * Indicates whether the integrations loading is in progress. + */ + isIntegrationsLoading?: boolean; } /** * Table Component for displaying SIEM rules migrations */ export const MigrationRulesTable: React.FC = React.memo( - ({ migrationId }) => { + ({ migrationId, integrations, isIntegrationsLoading }) => { const { addError } = useAppToasts(); const [pageIndex, setPageIndex] = useState(0); @@ -221,13 +232,35 @@ export const MigrationRulesTable: React.FC = React.mem [installSingleRule, isLoading] ); - const getMigrationRule = useCallback( + const getMigrationRuleData = useCallback( (ruleId: string) => { if (!isLoading && ruleMigrations.length) { - return ruleMigrations.find((item) => item.id === ruleId); + const ruleMigration = ruleMigrations.find((item) => item.id === ruleId); + let matchedPrebuiltRule: RuleResponse | undefined; + const relatedIntegrations: RelatedIntegration[] = []; + if (ruleMigration) { + // Find matched prebuilt rule if any and prioritize its installed version + const matchedPrebuiltRuleVersion = ruleMigration.elastic_rule?.prebuilt_rule_id + ? prebuiltRules[ruleMigration.elastic_rule.prebuilt_rule_id] + : undefined; + matchedPrebuiltRule = + matchedPrebuiltRuleVersion?.current ?? matchedPrebuiltRuleVersion?.target; + + if (integrations) { + if (matchedPrebuiltRule?.related_integrations) { + relatedIntegrations.push(...matchedPrebuiltRule.related_integrations); + } else if (ruleMigration.elastic_rule?.integration_id) { + const integration = integrations[ruleMigration.elastic_rule.integration_id]; + if (integration) { + relatedIntegrations.push(integration); + } + } + } + } + return { ruleMigration, matchedPrebuiltRule, relatedIntegrations, isIntegrationsLoading }; } }, - [isLoading, ruleMigrations] + [integrations, isIntegrationsLoading, isLoading, prebuiltRules, ruleMigrations] ); const { @@ -235,8 +268,7 @@ export const MigrationRulesTable: React.FC = React.mem openMigrationRuleDetails: openRulePreview, } = useMigrationRuleDetailsFlyout({ isLoading, - prebuiltRules, - getMigrationRule, + getMigrationRuleData, ruleActionsFactory, }); @@ -244,6 +276,7 @@ export const MigrationRulesTable: React.FC = React.mem disableActions: isTableLoading, openMigrationRuleDetails: openRulePreview, installMigrationRule: installSingleRule, + getMigrationRuleData, }); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx index 23980f5612f89..cd762db00e2c7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx @@ -34,6 +34,7 @@ export const createAuthorColumn = (): TableColumn => { return ; }, sortable: true, + truncateText: true, width: '10%', }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx new file mode 100644 index 0000000000000..43b7086c9814c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx @@ -0,0 +1,41 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import type { RelatedIntegration } from '../../../../../common/api/detection_engine'; +import { IntegrationsPopover } from '../../../../detections/components/rules/related_integrations/integrations_popover'; +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; +import type { TableColumn } from './constants'; + +export const createIntegrationsColumn = ({ + getMigrationRuleData, +}: { + getMigrationRuleData: ( + ruleId: string + ) => { relatedIntegrations?: RelatedIntegration[]; isIntegrationsLoading?: boolean } | undefined; +}): TableColumn => { + return { + field: 'elastic_rule.integration_id', + name: i18n.COLUMN_INTEGRATIONS, + render: (_, rule: RuleMigration) => { + const migrationRuleData = getMigrationRuleData(rule.id); + if (migrationRuleData?.isIntegrationsLoading) { + return ; + } + const relatedIntegrations = migrationRuleData?.relatedIntegrations; + if (relatedIntegrations == null || relatedIntegrations.length === 0) { + return null; + } + return ; + }, + truncateText: true, + width: '143px', + align: 'center', + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts index 64e459a609143..5b4fd6d6a477e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts @@ -97,3 +97,10 @@ export const COLUMN_UPDATED = i18n.translate( defaultMessage: 'Updated', } ); + +export const COLUMN_INTEGRATIONS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.integrationsLabel', + { + defaultMessage: 'Integrations', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx index cec9f86eb7bde..aaf4e75ac4917 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx @@ -19,7 +19,7 @@ export const createUpdatedColumn = (): TableColumn => { ), sortable: true, - truncateText: false, + truncateText: true, align: 'center', width: '10%', }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx index 4efaa4aba7181..9dad5a30ab073 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx @@ -8,16 +8,18 @@ import type { ReactNode } from 'react'; import React, { useCallback, useState, useMemo } from 'react'; import type { EuiTabbedContentTab } from '@elastic/eui'; -import type { - PrebuiltRuleVersion, - RuleMigration, -} from '../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; import { MigrationRuleDetailsFlyout } from '../components/rule_details_flyout'; interface UseMigrationRuleDetailsFlyoutParams { isLoading?: boolean; - prebuiltRules: Record; - getMigrationRule: (ruleId: string) => RuleMigration | undefined; + getMigrationRuleData: (ruleId: string) => + | { + ruleMigration?: RuleMigration; + matchedPrebuiltRule?: RuleResponse; + } + | undefined; ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode; extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[]; } @@ -30,27 +32,17 @@ interface UseMigrationRuleDetailsFlyoutResult { export function useMigrationRuleDetailsFlyout({ isLoading, - prebuiltRules, - getMigrationRule, + getMigrationRuleData, extraTabsFactory, ruleActionsFactory, }: UseMigrationRuleDetailsFlyoutParams): UseMigrationRuleDetailsFlyoutResult { const [migrationRuleId, setMigrationRuleId] = useState(); - const ruleMigration = useMemo(() => { + const migrationRuleData = useMemo(() => { if (migrationRuleId) { - return getMigrationRule(migrationRuleId); + return getMigrationRuleData(migrationRuleId); } - }, [getMigrationRule, migrationRuleId]); - const matchedPrebuiltRule = useMemo(() => { - if (ruleMigration) { - // Find matched prebuilt rule if any and prioritize its installed version - const matchedPrebuiltRuleVersion = ruleMigration.elastic_rule?.prebuilt_rule_id - ? prebuiltRules[ruleMigration.elastic_rule.prebuilt_rule_id] - : undefined; - return matchedPrebuiltRuleVersion?.current ?? matchedPrebuiltRuleVersion?.target; - } - }, [prebuiltRules, ruleMigration]); + }, [getMigrationRuleData, migrationRuleId]); const openMigrationRuleDetails = useCallback((rule: RuleMigration) => { setMigrationRuleId(rule.id); @@ -58,19 +50,24 @@ export function useMigrationRuleDetailsFlyout({ const closeMigrationRuleDetails = useCallback(() => setMigrationRuleId(undefined), []); const ruleActions = useMemo( - () => ruleMigration && ruleActionsFactory(ruleMigration, closeMigrationRuleDetails), - [ruleMigration, ruleActionsFactory, closeMigrationRuleDetails] + () => + migrationRuleData?.ruleMigration && + ruleActionsFactory(migrationRuleData.ruleMigration, closeMigrationRuleDetails), + [migrationRuleData?.ruleMigration, ruleActionsFactory, closeMigrationRuleDetails] ); const extraTabs = useMemo( - () => (ruleMigration && extraTabsFactory ? extraTabsFactory(ruleMigration) : []), - [ruleMigration, extraTabsFactory] + () => + migrationRuleData?.ruleMigration && extraTabsFactory + ? extraTabsFactory(migrationRuleData.ruleMigration) + : [], + [extraTabsFactory, migrationRuleData?.ruleMigration] ); return { - migrationRuleDetailsFlyout: ruleMigration && ( + migrationRuleDetailsFlyout: migrationRuleData?.ruleMigration && ( void; installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void; + getMigrationRuleData: ( + ruleId: string + ) => { relatedIntegrations?: RelatedIntegration[]; isIntegrationsLoading?: boolean } | undefined; }): TableColumn[] => { return useMemo( () => [ @@ -35,12 +41,13 @@ export const useMigrationRulesTableColumns = ({ createRiskScoreColumn(), createSeverityColumn(), createAuthorColumn(), + createIntegrationsColumn({ getMigrationRuleData }), createActionsColumn({ disableActions, openMigrationRuleDetails, installMigrationRule, }), ], - [disableActions, installMigrationRule, openMigrationRuleDetails] + [disableActions, getMigrationRuleData, installMigrationRule, openMigrationRuleDetails] ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx index 3877a6f46cbe7..4fd24c709382f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useMemo } from 'react'; import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui'; import type { RouteComponentProps } from 'react-router-dom'; +import type { RelatedIntegration } from '../../../../common/api/detection_engine'; import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants'; import { useNavigation } from '../../../common/lib/kibana'; import { HeaderPage } from '../../../common/components/header_page'; @@ -22,6 +23,7 @@ import { MissingPrivilegesCallOut } from '../../../detections/components/callout import { HeaderButtons } from '../components/header_buttons'; import { UnknownMigration } from '../components/unknown_migration'; import { useLatestStats } from '../service/hooks/use_latest_stats'; +import { useGetIntegrations } from '../service/hooks/use_get_integrations'; type MigrationRulesPageProps = RouteComponentProps<{ migrationId?: string }>; @@ -35,6 +37,16 @@ export const MigrationRulesPage: React.FC = React.memo( const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } = useLatestStats(); + const [integrations, setIntegrations] = React.useState< + Record | undefined + >(); + const { getIntegrations, isLoading: isIntegrationsLoading } = + useGetIntegrations(setIntegrations); + + useEffect(() => { + getIntegrations(); + }, [getIntegrations]); + const finishedRuleMigrationsStats = useMemo(() => { if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) { return []; @@ -72,8 +84,14 @@ export const MigrationRulesPage: React.FC = React.memo( if (!migrationId || !finishedRuleMigrationsStats.some((stats) => stats.id === migrationId)) { return ; } - return ; - }, [migrationId, finishedRuleMigrationsStats]); + return ( + + ); + }, [migrationId, finishedRuleMigrationsStats, integrations, isIntegrationsLoading]); return ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_integrations.ts new file mode 100644 index 0000000000000..8ed94e78f31c4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_integrations.ts @@ -0,0 +1,42 @@ +/* + * 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 { RelatedIntegration } from '../../../../../common/api/detection_engine'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { reducer, initialState } from './common/api_request_reducer'; + +export const GET_INTEGRATIONS_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.getIntegrationsError', + { defaultMessage: 'Failed to fetch integrations' } +); + +export type OnSuccess = (integrations: Record) => void; + +export const useGetIntegrations = (onSuccess: OnSuccess) => { + const { siemMigrations, notifications } = useKibana().services; + const [state, dispatch] = useReducer(reducer, initialState); + + const getIntegrations = useCallback(() => { + (async () => { + try { + dispatch({ type: 'start' }); + const integrations = await siemMigrations.rules.getIntegrations(); + + onSuccess(integrations); + dispatch({ type: 'success' }); + } catch (err) { + const apiError = err.body ?? err; + notifications.toasts.addError(apiError, { title: GET_INTEGRATIONS_ERROR }); + dispatch({ type: 'error', error: apiError }); + } + })(); + }, [siemMigrations.rules, notifications.toasts, onSuccess]); + + return { isLoading: state.loading, error: state.error, getIntegrations }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 83ead556b09cc..69ed039e6d5ab 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -12,6 +12,7 @@ import { DEFAULT_ASSISTANT_NAMESPACE, TRACE_OPTIONS_SESSION_STORAGE_KEY, } from '@kbn/elastic-assistant/impl/assistant_context/constants'; +import type { RelatedIntegration } from '../../../../common/api/detection_engine'; import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen'; import type { RuleMigrationResourceData, @@ -35,6 +36,7 @@ import { type GetRuleMigrationsStatsAllParams, getMissingResources, upsertMigrationResources, + getIntegrations, } from '../api'; import type { RuleMigrationStats } from '../types'; import { getSuccessToast } from './success_notification'; @@ -181,6 +183,10 @@ export class SiemRulesMigrationsService { }); } + public async getIntegrations(): Promise> { + return getIntegrations({}); + } + private async startTaskStatsPolling(): Promise { let pendingMigrationIds: string[] = []; do { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts new file mode 100644 index 0000000000000..afc7c7b9608d3 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts @@ -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 type { IKibanaResponse, Logger } from '@kbn/core/server'; +import type { RelatedIntegration } from '../../../../../common/api/detection_engine'; +import { type GetRuleMigrationIntegrationsResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { withLicense } from './util/with_license'; + +export const registerSiemRuleMigrationsIntegrationsRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { + version: '1', + validate: {}, + }, + withLicense( + async ( + context, + req, + res + ): Promise> => { + try { + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + + const relatedIntegrations: Record = {}; + const packages = await ruleMigrationsClient.data.integrations.getIntegrationPackages(); + packages?.forEach(({ id, version, integration }) => { + relatedIntegrations[id] = { package: id, version, integration }; + }); + + return res.ok({ body: relatedIntegrations }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ) + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts index 551e4a51e477e..8165b858e2a31 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts @@ -12,8 +12,7 @@ import { GetRuleMigrationPrebuiltRulesRequestParams } from '../../../../../commo import { SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { withLicense } from './util/with_license'; -import { getPrebuiltRules, getUniquePrebuiltRuleIds } from './util/prebuilt_rules'; -import { MAX_PREBUILT_RULES_TO_FETCH } from './constants'; +import { getPrebuiltRulesForMigration } from './util/prebuilt_rules'; export const registerSiemRuleMigrationsPrebuiltRulesRoute = ( router: SecuritySolutionPluginRouter, @@ -47,19 +46,11 @@ export const registerSiemRuleMigrationsPrebuiltRulesRoute = ( const savedObjectsClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); - const result = await ruleMigrationsClient.data.rules.get(migrationId, { - filters: { - prebuilt: true, - }, - from: 0, - size: MAX_PREBUILT_RULES_TO_FETCH, - }); - - const prebuiltRulesIds = getUniquePrebuiltRuleIds(result.data); - const prebuiltRules = await getPrebuiltRules( + const prebuiltRules = await getPrebuiltRulesForMigration( + migrationId, + ruleMigrationsClient, rulesClient, - savedObjectsClient, - prebuiltRulesIds + savedObjectsClient ); return res.ok({ body: prebuiltRules }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts index 241e59ac02a27..05d2c8f1a5dc4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts @@ -22,6 +22,7 @@ import { registerSiemRuleMigrationsInstallRoute } from './install'; import { registerSiemRuleMigrationsInstallTranslatedRoute } from './install_translated'; import { registerSiemRuleMigrationsResourceGetMissingRoute } from './resources/missing'; import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rules'; +import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, @@ -39,6 +40,7 @@ export const registerSiemRuleMigrationsRoutes = ( registerSiemRuleMigrationsStopRoute(router, logger); registerSiemRuleMigrationsInstallRoute(router, logger); registerSiemRuleMigrationsInstallTranslatedRoute(router, logger); + registerSiemRuleMigrationsIntegrationsRoute(router, logger); registerSiemRuleMigrationsResourceUpsertRoute(router, logger); registerSiemRuleMigrationsResourceGetRoute(router, logger); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts index 7760612abc878..cf7317f0bfde0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts @@ -13,6 +13,7 @@ import { fetchRuleVersionsTriad } from '../../../../detection_engine/prebuilt_ru import { createPrebuiltRuleAssetsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; import { convertPrebuiltRuleAssetToRuleResponse } from '../../../../detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { SiemRuleMigrationsClient } from '../../siem_rule_migrations_service'; export const getUniquePrebuiltRuleIds = (migrationRules: RuleMigration[]): string[] => { const rulesIds = new Set(); @@ -82,3 +83,37 @@ export const getPrebuiltRules = async ( return prebuiltRules; }; + +/** + * Gets Elastic prebuilt rules + * @param migrationId The `id` of the migration to get related prebuilt rules for + * @param ruleMigrationsClient The rules migration client to migration rules data + * @param rulesClient The rules client to fetch prebuilt rules + * @param savedObjectsClient The saved objects client + * @returns + */ +export const getPrebuiltRulesForMigration = async ( + migrationId: string, + ruleMigrationsClient: SiemRuleMigrationsClient, + rulesClient: RulesClient, + savedObjectsClient: SavedObjectsClientContract +): Promise> => { + const options = { filters: { prebuilt: true } }; + const batches = ruleMigrationsClient.data.rules.searchBatches(migrationId, options); + + const rulesIds = new Set(); + let results = await batches.next(); + while (results.length) { + results.forEach((rule) => { + if (rule.elastic_rule?.prebuilt_rule_id) { + rulesIds.add(rule.elastic_rule.prebuilt_rule_id); + } + }); + results = await batches.next(); + } + const prebuiltRulesIds = Array.from(rulesIds); + + const prebuiltRules = await getPrebuiltRules(rulesClient, savedObjectsClient, prebuiltRulesIds); + + return prebuiltRules; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts index c06c889482360..e479c42cce273 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts @@ -6,6 +6,7 @@ */ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { PackageService } from '@kbn/fleet-plugin/server'; import { RuleMigrationsDataIntegrationsClient } from './rule_migrations_data_integrations_client'; import { RuleMigrationsDataPrebuiltRulesClient } from './rule_migrations_data_prebuilt_rules_client'; import { RuleMigrationsDataResourcesClient } from './rule_migrations_data_resources_client'; @@ -25,7 +26,8 @@ export class RuleMigrationsDataClient { indexNameProviders: IndexNameProviders, username: string, esClient: ElasticsearchClient, - logger: Logger + logger: Logger, + packageService?: PackageService ) { this.rules = new RuleMigrationsDataRulesClient( indexNameProviders.rules, @@ -43,7 +45,8 @@ export class RuleMigrationsDataClient { indexNameProviders.integrations, username, esClient, - logger + logger, + packageService ); this.prebuiltRules = new RuleMigrationsDataPrebuiltRulesClient( indexNameProviders.prebuiltrules, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts index fdb063836f9e4..947a206cd0c7a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts @@ -5,11 +5,15 @@ * 2.0. */ +import type { PackageService } from '@kbn/fleet-plugin/server'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { PackageList } from '@kbn/fleet-plugin/common'; import type { Integration } from '../types'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; /* This will be removed once the package registry changes is performed */ import integrationsFile from './integrations_temp.json'; +import type { IndexNameProvider } from './rule_migrations_data_client'; /* The minimum score required for a integration to be considered correct, might need to change this later */ const MIN_SCORE = 40 as const; @@ -22,6 +26,20 @@ const INTEGRATIONS = integrationsFile as Integration[]; * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. */ export class RuleMigrationsDataIntegrationsClient extends RuleMigrationsDataBaseClient { + constructor( + getIndexName: IndexNameProvider, + username: string, + esClient: ElasticsearchClient, + logger: Logger, + private packageService?: PackageService + ) { + super(getIndexName, username, esClient, logger); + } + + async getIntegrationPackages(): Promise { + return this.packageService?.asInternalUser.getPackages(); + } + /** Indexes an array of integrations to be used with ELSER semantic search queries */ async create(): Promise { const index = await this.getIndexName(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 47bcd56e6433e..8728929a75ba0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -43,6 +43,7 @@ export interface RuleMigrationFilters { ids?: string[]; installable?: boolean; prebuilt?: boolean; + custom?: boolean; searchTerm?: string; } export interface RuleMigrationGetOptions { @@ -397,7 +398,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient private getFilterQuery( migrationId: string, - { status, ids, installable, prebuilt, searchTerm }: RuleMigrationFilters = {} + { status, ids, installable, prebuilt, custom, searchTerm }: RuleMigrationFilters = {} ): QueryDslQueryContainer { const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }]; if (status) { @@ -416,6 +417,9 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient if (prebuilt) { filter.push(searchConditions.isPrebuilt()); } + if (custom) { + filter.push(searchConditions.isCustom()); + } if (searchTerm?.length) { filter.push(searchConditions.matchTitle(searchTerm)); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts index 5799e5ab84c07..6681f0c3903b0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts @@ -6,6 +6,7 @@ */ import type { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server'; import { IndexPatternAdapter, type FieldMap, type InstallParams } from '@kbn/index-adapter'; +import type { PackageService } from '@kbn/fleet-plugin/server'; import type { IndexNameProvider, IndexNameProviders } from './rule_migrations_data_client'; import { RuleMigrationsDataClient } from './rule_migrations_data_client'; import { @@ -24,6 +25,7 @@ interface CreateClientParams { spaceId: string; currentUser: AuthenticatedUser; esClient: ElasticsearchClient; + packageService?: PackageService; } export class RuleMigrationsDataService { @@ -58,7 +60,7 @@ export class RuleMigrationsDataService { ]); } - public createClient({ spaceId, currentUser, esClient }: CreateClientParams) { + public createClient({ spaceId, currentUser, esClient, packageService }: CreateClientParams) { const indexNameProviders: IndexNameProviders = { rules: this.createIndexNameProvider('rules', spaceId), resources: this.createIndexNameProvider('resources', spaceId), @@ -70,7 +72,8 @@ export class RuleMigrationsDataService { indexNameProviders, currentUser.username, esClient, - this.logger + this.logger, + packageService ); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts index 3bd8da066a45f..18196246da66d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts @@ -28,6 +28,14 @@ export const conditions = { }, }; }, + isCustom(): QueryDslQueryContainer { + return { + nested: { + path: 'elastic_rule', + query: { bool: { must_not: { exists: { field: 'elastic_rule.prebuilt_rule_id' } } } }, + }, + }; + }, matchTitle(title: string): QueryDslQueryContainer { return { nested: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts index 2d0ef644b8e56..6f3fcd46612d4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts @@ -113,9 +113,21 @@ const sortingOptionsMap: { [key: string]: (direction?: estypes.SortOrder) => estypes.SortCombinations[]; } = { 'elastic_rule.title': sortingOptions.name, - 'elastic_rule.severity': sortingOptions.severity, - 'elastic_rule.prebuilt_rule_id': sortingOptions.matchedPrebuiltRule, - translation_result: sortingOptions.status, + 'elastic_rule.severity': (direction?: estypes.SortOrder) => [ + ...sortingOptions.severity(direction), + ...sortingOptions.status('desc'), + ...sortingOptions.matchedPrebuiltRule('desc'), + ], + 'elastic_rule.prebuilt_rule_id': (direction?: estypes.SortOrder) => [ + ...sortingOptions.matchedPrebuiltRule(direction), + ...sortingOptions.status('desc'), + ...sortingOptions.severity('desc'), + ], + translation_result: (direction?: estypes.SortOrder) => [ + ...sortingOptions.status(direction), + ...sortingOptions.matchedPrebuiltRule('desc'), + ...sortingOptions.severity('desc'), + ], updated_at: sortingOptions.updated, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts index d9f4a1c5249cb..3be54a7e3d896 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts @@ -14,6 +14,7 @@ import type { KibanaRequest, Logger, } from '@kbn/core/server'; +import type { PackageService } from '@kbn/fleet-plugin/server'; import { RuleMigrationsDataService } from './data/rule_migrations_data_service'; import type { RuleMigrationsDataClient } from './data/rule_migrations_data_client'; import type { RuleMigrationsTaskClient } from './task/rule_migrations_task_client'; @@ -29,6 +30,7 @@ export interface SiemRuleMigrationsCreateClientParams { request: KibanaRequest; currentUser: AuthenticatedUser | null; spaceId: string; + packageService?: PackageService; } export interface SiemRuleMigrationsClient { @@ -60,13 +62,19 @@ export class SiemRuleMigrationsService { createClient({ spaceId, currentUser, + packageService, request, }: SiemRuleMigrationsCreateClientParams): SiemRuleMigrationsClient { assert(currentUser, 'Current user must be authenticated'); assert(this.esClusterClient, 'ES client not available, please call setup first'); const esClient = this.esClusterClient.asInternalUser; - const dataClient = this.dataService.createClient({ spaceId, currentUser, esClient }); + const dataClient = this.dataService.createClient({ + spaceId, + currentUser, + esClient, + packageService, + }); const taskClient = this.taskService.createClient({ currentUser, dataClient }); return { data: dataClient, task: taskClient }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts index a6b5e1b3e650a..1c5e954dfeb79 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts @@ -176,6 +176,7 @@ export class RequestContextFactory implements IRequestContextFactory { request, currentUser: coreContext.security.authc.getCurrentUser(), spaceId: getSpaceId(), + packageService: startPlugins.fleet?.packageService, }) ), 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 9ffdc1c43a2a1..d2b44d297b276 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 @@ -974,6 +974,16 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + /** + * Retrieves all related integrations + */ + getRuleMigrationIntegrations(kibanaSpace: string = 'default') { + return supertest + .get(routeWithNamespace('/internal/siem_migrations/rules/integrations', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Retrieves all available prebuilt rules (installed and installable) */