[Rules migration] Add Integrations column (#11387) (#204639)

## Summary

[Internal link](https://github.com/elastic/security-team/issues/10820)
to the feature details

These changes add a functionality which enables related integrations
functionality for migration rules:
* related integration are shown in the migration rules table
* user can navigate to the integration page to see instructions about
installation process

### Other tasks and fixes

* Default sorting in the table (by `Stats` => by `Author` => by
`Severity` => by `Updated`)

> [!NOTE]  
> This feature needs `siemMigrationsEnabled` experimental flag enabled
to work.

## Screen recording

<img width="1838" alt="Screenshot 2024-12-17 at 19 26 47"
src="https://github.com/user-attachments/assets/c1ed9d5d-e237-4dfe-b144-a80adbf46cd3"
/>

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ievgen Sorokopud 2025-01-08 16:57:24 +01:00 committed by GitHub
parent aa6489585b
commit 019f0e8414
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 425 additions and 59 deletions

View file

@ -367,6 +367,7 @@ import type {
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
GetRuleMigrationResponse,
GetRuleMigrationIntegrationsResponse,
GetRuleMigrationPrebuiltRulesRequestParamsInput,
GetRuleMigrationPrebuiltRulesResponse,
GetRuleMigrationResourcesRequestQueryInput,
@ -1458,6 +1459,21 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retrieves all related integrations
*/
async getRuleMigrationIntegrations() {
this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationIntegrations`);
return this.kbnClient
.request<GetRuleMigrationIntegrationsResponse>({
path: '/internal/siem_migrations/rules/integrations',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'GET',
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retrieves all available prebuilt rules (installed and installable)
*/

View file

@ -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;

View file

@ -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
>;

View file

@ -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}:

View file

@ -24,6 +24,7 @@ import {
SIEM_RULE_MIGRATION_RESOURCES_PATH,
SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH,
SIEM_RULE_MIGRATION_RETRY_PATH,
SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH,
} from '../../../../common/siem_migrations/constants';
import type {
CreateRuleMigrationRequestBody,
@ -43,6 +44,7 @@ import type {
RetryRuleMigrationRequestBody,
StartRuleMigrationResponse,
RetryRuleMigrationResponse,
GetRuleMigrationIntegrationsResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
export interface GetRuleMigrationStatsParams {
@ -320,6 +322,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<GetRuleMigrationIntegrationsResponse> => {
return KibanaServices.get().http.get<GetRuleMigrationIntegrationsResponse>(
SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH,
{ version: '1', signal }
);
};
export interface UpdateRulesParams {
/** The list of migration rules data to update */
rulesToUpdate: UpdateRuleMigrationData[];

View file

@ -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';
@ -49,13 +50,23 @@ export interface MigrationRulesTableProps {
* Re-fetches latest rule migration data
*/
refetchData?: () => void;
/**
* Existing integrations.
*/
integrations?: Record<string, RelatedIntegration>;
/**
* Indicates whether the integrations loading is in progress.
*/
isIntegrationsLoading?: boolean;
}
/**
* Table Component for displaying SIEM rules migrations
*/
export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.memo(
({ migrationId, refetchData }) => {
({ migrationId, refetchData, integrations, isIntegrationsLoading }) => {
const { addError } = useAppToasts();
const [pageIndex, setPageIndex] = useState(0);
@ -233,13 +244,35 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = 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 {
@ -247,8 +280,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
openMigrationRuleDetails: openRulePreview,
} = useMigrationRuleDetailsFlyout({
isLoading,
prebuiltRules,
getMigrationRule,
getMigrationRuleData,
ruleActionsFactory,
});
@ -256,6 +288,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
disableActions: isTableLoading,
openMigrationRuleDetails: openRulePreview,
installMigrationRule: installSingleRule,
getMigrationRuleData,
});
return (

View file

@ -39,6 +39,7 @@ export const createAuthorColumn = (): TableColumn => {
);
},
sortable: true,
truncateText: true,
width: '10%',
};
};

View file

@ -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 <EuiLoadingSpinner />;
}
const relatedIntegrations = migrationRuleData?.relatedIntegrations;
if (relatedIntegrations == null || relatedIntegrations.length === 0) {
return null;
}
return <IntegrationsPopover relatedIntegrations={relatedIntegrations} />;
},
truncateText: true,
width: '143px',
align: 'center',
};
};

View file

@ -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',
}
);

View file

@ -19,7 +19,7 @@ export const createUpdatedColumn = (): TableColumn => {
<FormattedRelativePreferenceDate value={value} dateFormat="M/D/YY" />
),
sortable: true,
truncateText: false,
truncateText: true,
align: 'center',
width: '10%',
};

View file

@ -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<string, PrebuiltRuleVersion>;
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<string | undefined>();
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 && (
<MigrationRuleDetailsFlyout
ruleMigration={ruleMigration}
matchedPrebuiltRule={matchedPrebuiltRule}
ruleMigration={migrationRuleData.ruleMigration}
matchedPrebuiltRule={migrationRuleData.matchedPrebuiltRule}
size="l"
closeFlyout={closeMigrationRuleDetails}
ruleActions={ruleActions}

View file

@ -6,6 +6,7 @@
*/
import { useMemo } from 'react';
import type { RelatedIntegration } from '../../../../common/api/detection_engine';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { TableColumn } from '../components/rules_table_columns';
import {
@ -17,15 +18,20 @@ import {
createStatusColumn,
createUpdatedColumn,
} from '../components/rules_table_columns';
import { createIntegrationsColumn } from '../components/rules_table_columns/integrations';
export const useMigrationRulesTableColumns = ({
disableActions,
openMigrationRuleDetails,
installMigrationRule,
getMigrationRuleData,
}: {
disableActions?: boolean;
openMigrationRuleDetails: (rule: 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]
);
};

View file

@ -9,6 +9,7 @@ import React, { useCallback, 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';
@ -27,6 +28,7 @@ import { MigrationReadyPanel } from '../components/migration_status_panels/migra
import { MigrationProgressPanel } from '../components/migration_status_panels/migration_progress_panel';
import { useInvalidateGetMigrationRules } from '../logic/use_get_migration_rules';
import { useInvalidateGetMigrationTranslationStats } from '../logic/use_get_migration_translation_stats';
import { useGetIntegrations } from '../service/hooks/use_get_integrations';
type MigrationRulesPageProps = RouteComponentProps<{ migrationId?: string }>;
@ -39,6 +41,16 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
const { navigateTo } = useNavigation();
const { data: ruleMigrationsStats, isLoading, refreshStats } = useLatestStats();
const [integrations, setIntegrations] = React.useState<
Record<string, RelatedIntegration> | undefined
>();
const { getIntegrations, isLoading: isIntegrationsLoading } =
useGetIntegrations(setIntegrations);
useEffect(() => {
getIntegrations();
}, [getIntegrations]);
useEffect(() => {
if (isLoading) {
return;
@ -85,7 +97,14 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
return <UnknownMigration />;
}
if (migrationStats.status === SiemMigrationTaskStatus.FINISHED) {
return <MigrationRulesTable migrationId={migrationId} refetchData={refetchData} />;
return (
<MigrationRulesTable
migrationId={migrationId}
refetchData={refetchData}
integrations={integrations}
isIntegrationsLoading={isIntegrationsLoading}
/>
);
}
return (
<RuleMigrationDataInputWrapper onFlyoutClosed={refetchData}>
@ -99,7 +118,7 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
</>
</RuleMigrationDataInputWrapper>
);
}, [migrationId, refetchData, ruleMigrationsStats]);
}, [migrationId, refetchData, ruleMigrationsStats, integrations, isIntegrationsLoading]);
return (
<>

View file

@ -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<string, RelatedIntegration>) => 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 };
};

View file

@ -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,
@ -37,6 +38,7 @@ import {
getMissingResources,
upsertMigrationResources,
retryRuleMigration,
getIntegrations,
} from '../api';
import type { RetryRuleMigrationFilter, RuleMigrationStats } from '../types';
import { getSuccessToast } from './success_notification';
@ -211,6 +213,10 @@ export class SiemRulesMigrationsService {
});
}
public async getIntegrations(): Promise<Record<string, RelatedIntegration>> {
return getIntegrations({});
}
private async startTaskStatsPolling(): Promise<void> {
let pendingMigrationIds: string[] = [];
do {

View file

@ -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<IKibanaResponse<GetRuleMigrationIntegrationsResponse>> => {
try {
const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const relatedIntegrations: Record<string, RelatedIntegration> = {};
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 });
}
}
)
);
};

View file

@ -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 });

View file

@ -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);

View file

@ -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<string>();
@ -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<Record<string, PrebuiltRulesResults>> => {
const options = { filters: { prebuilt: true } };
const batches = ruleMigrationsClient.data.rules.searchBatches(migrationId, options);
const rulesIds = new Set<string>();
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;
};

View file

@ -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,

View file

@ -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<PackageList | undefined> {
return this.packageService?.asInternalUser.getPackages();
}
/** Indexes an array of integrations to be used with ELSER semantic search queries */
async create(): Promise<void> {
const index = await this.getIndexName();

View file

@ -43,6 +43,7 @@ export interface RuleMigrationFilters {
ids?: string[];
installable?: boolean;
prebuilt?: boolean;
custom?: boolean;
failed?: boolean;
notFullyTranslated?: boolean;
searchTerm?: string;
@ -404,6 +405,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
ids,
installable,
prebuilt,
custom,
searchTerm,
failed,
notFullyTranslated,
@ -426,6 +428,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));
}

View file

@ -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
);
}

View file

@ -34,6 +34,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: {

View file

@ -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,
};

View file

@ -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 };

View file

@ -176,6 +176,7 @@ export class RequestContextFactory implements IRequestContextFactory {
request,
currentUser: coreContext.security.authc.getCurrentUser(),
spaceId: getSpaceId(),
packageService: startPlugins.fleet?.packageService,
})
),

View file

@ -978,6 +978,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)
*/