mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
## 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:
parent
aa6489585b
commit
019f0e8414
28 changed files with 425 additions and 59 deletions
|
@ -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)
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
>;
|
||||
|
|
|
@ -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}:
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -39,6 +39,7 @@ export const createAuthorColumn = (): TableColumn => {
|
|||
);
|
||||
},
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '10%',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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%',
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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 });
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -176,6 +176,7 @@ export class RequestContextFactory implements IRequestContextFactory {
|
|||
request,
|
||||
currentUser: coreContext.security.authc.getCurrentUser(),
|
||||
spaceId: getSpaceId(),
|
||||
packageService: startPlugins.fleet?.packageService,
|
||||
})
|
||||
),
|
||||
|
||||
|
|
|
@ -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)
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue