Skip to content

Commit

Permalink
[Rules migration] Add Integrations column (#11387) (#204639)
Browse files Browse the repository at this point in the history
## Summary

[Internal link](elastic/security-team#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 <[email protected]>
  • Loading branch information
e40pud and kibanamachine authored Jan 8, 2025
1 parent aa64895 commit 019f0e8
Show file tree
Hide file tree
Showing 28 changed files with 425 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ import type {
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
GetRuleMigrationResponse,
GetRuleMigrationIntegrationsResponse,
GetRuleMigrationPrebuiltRulesRequestParamsInput,
GetRuleMigrationPrebuiltRulesResponse,
GetRuleMigrationResourcesRequestQueryInput,
Expand Down Expand Up @@ -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)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -43,6 +44,7 @@ import type {
RetryRuleMigrationRequestBody,
StartRuleMigrationResponse,
RetryRuleMigrationResponse,
GetRuleMigrationIntegrationsResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';

export interface GetRuleMigrationStatsParams {
Expand Down Expand Up @@ -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[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -233,29 +244,51 @@ 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 {
migrationRuleDetailsFlyout: rulePreviewFlyout,
openMigrationRuleDetails: openRulePreview,
} = useMigrationRuleDetailsFlyout({
isLoading,
prebuiltRules,
getMigrationRule,
getMigrationRuleData,
ruleActionsFactory,
});

const rulesColumns = useMigrationRulesTableColumns({
disableActions: isTableLoading,
openMigrationRuleDetails: openRulePreview,
installMigrationRule: installSingleRule,
getMigrationRuleData,
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const createAuthorColumn = (): TableColumn => {
);
},
sortable: true,
truncateText: true,
width: '10%',
};
};
Original file line number Diff line number Diff line change
@@ -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',
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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%',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand All @@ -30,47 +32,42 @@ 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);
}, []);
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}
Expand Down
Loading

0 comments on commit 019f0e8

Please sign in to comment.