Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Rules migration][UI] Basic rule migrations UI (#10820) #200978

Merged
merged 10 commits into from
Nov 22, 2024
1 change: 1 addition & 0 deletions packages/deeplinks/security/deep_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export enum SecurityPageName {
rulesAdd = 'rules-add',
rulesCreate = 'rules-create',
rulesLanding = 'rules-landing',
rulesSiemMigrations = 'rules-siem-migrations',
/*
* Warning: Computed values are not permitted in an enum with string valued members
* All threat intelligence page names must match `TIPageId` in x-pack/plugins/threat_intelligence/public/common/navigation/types.ts
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const RULES_LANDING_PATH = `${RULES_PATH}/landing` as const;
export const RULES_ADD_PATH = `${RULES_PATH}/add_rules` as const;
export const RULES_UPDATES = `${RULES_PATH}/updates` as const;
export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const;
export const SIEM_MIGRATIONS_PATH = `${RULES_PATH}/siem_migrations` as const;
export const EXCEPTIONS_PATH = '/exceptions' as const;
export const EXCEPTION_LIST_DETAIL_PATH = `${EXCEPTIONS_PATH}/details/:detailName` as const;
export const HOSTS_PATH = '/hosts' as const;
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/public/app/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exce
defaultMessage: 'Shared exception lists',
});

export const SIEM_MIGRATIONS = i18n.translate('xpack.securitySolution.navigation.siemMigrations', {
defaultMessage: 'SIEM Rules Migrations',
});

export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', {
defaultMessage: 'Alerts',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import { INTERNAL_ALERTING_API_FIND_RULES_PATH } from '@kbn/alerting-plugin/comm
import { BASE_ACTION_API_PATH } from '@kbn/actions-plugin/common';
import type { ActionType, AsApiContract } from '@kbn/actions-plugin/common';
import type { ActionResult } from '@kbn/actions-plugin/server';
import { replaceParams } from '@kbn/openapi-common/shared';
import type {
GetAllStatsRuleMigrationResponse,
GetRuleMigrationResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import {
SIEM_RULE_MIGRATIONS_ALL_STATS_PATH,
SIEM_RULE_MIGRATION_PATH,
} from '../../../../common/siem_migrations/constants';
import { convertRulesFilterToKQL } from '../../../../common/detection_engine/rule_management/rule_filtering';
import type {
UpgradeSpecificRulesRequest,
Expand Down Expand Up @@ -698,3 +707,56 @@ export const bootstrapPrebuiltRules = async (): Promise<BootstrapPrebuiltRulesRe
method: 'POST',
version: '1',
});

/**
* SIEM Rules Migration routes
*/

/**
* Retrieves the stats for all the existing migrations, aggregated by `migration_id`.
*
* @param signal AbortSignal for cancelling request
*
* @throws An error if response is not OK
*/
export const getRuleMigrationsStatsAll = async ({
signal,
}: {
signal: AbortSignal | undefined;
}): Promise<GetAllStatsRuleMigrationResponse> =>
KibanaServices.get().http.fetch<GetAllStatsRuleMigrationResponse>(
SIEM_RULE_MIGRATIONS_ALL_STATS_PATH,
{
method: 'GET',
version: '1',
signal,
}
);

/**
* Retrieves all the migration rule documents of a specific migration.
*
* @param migrationId `id` of the migration to retrieve rule documents for
* @param signal AbortSignal for cancelling request
*
* @throws An error if response is not OK
*/
export const getRuleMigrations = async ({
migrationId,
signal,
}: {
migrationId?: string;
signal: AbortSignal | undefined;
}): Promise<GetRuleMigrationResponse> => {
if (!migrationId) {
return [];
}
return KibanaServices.get().http.fetch<GetRuleMigrationResponse>(
replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }),
{
method: 'GET',
version: '1',
signal,
}
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { replaceParams } from '@kbn/openapi-common/shared';
import type { GetRuleMigrationResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_PATH } from '../../../../../../common/siem_migrations/constants';
import { getRuleMigrations } from '../../api';
import { DEFAULT_QUERY_OPTIONS } from '../constants';

export const useGetRuleMigrationsQuery = (
migrationId?: string,
options?: UseQueryOptions<GetRuleMigrationResponse>
) => {
const SPECIFIC_MIGRATION_PATH = migrationId
? replaceParams(SIEM_RULE_MIGRATION_PATH, {
migration_id: migrationId,
})
: SIEM_RULE_MIGRATION_PATH;
return useQuery<GetRuleMigrationResponse>(
['GET', SPECIFIC_MIGRATION_PATH],
async ({ signal }) => {
return getRuleMigrations({ migrationId, signal });
},
{
...DEFAULT_QUERY_OPTIONS,
...options,
}
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { GetAllStatsRuleMigrationResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATIONS_ALL_STATS_PATH } from '../../../../../../common/siem_migrations/constants';
import { getRuleMigrationsStatsAll } from '../../api';
import { DEFAULT_QUERY_OPTIONS } from '../constants';

export const GET_RULE_MIGRATIONS_STATS_ALL_QUERY_KEY = ['GET', SIEM_RULE_MIGRATIONS_ALL_STATS_PATH];

export const useGetRuleMigrationsStatsAllQuery = (
options?: UseQueryOptions<GetAllStatsRuleMigrationResponse>
) => {
return useQuery<GetAllStatsRuleMigrationResponse>(
GET_RULE_MIGRATIONS_STATS_ALL_QUERY_KEY,
async ({ signal }) => {
return getRuleMigrationsStatsAll({ signal });
},
{
...DEFAULT_QUERY_OPTIONS,
...options,
}
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { Severity } from '@kbn/securitysolution-io-ts-alerting-types';

export const DEFAULT_TRANSLATION_RISK_SCORE = 21;
export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low';
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { EuiComboBoxOptionOption } from '@elastic/eui';
import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import { useSiemMigrationsTableContext } from './siem_migrations_table_context';
import * as i18n from './translations';

export const SiemMigrationsHeaderButtons = () => {
const {
state: { migrationsIds, selectedMigrationId },
actions: { onMigrationIdChange },
} = useSiemMigrationsTableContext();

const migrationOptions = useMemo(() => {
const options: Array<EuiComboBoxOptionOption<string>> = migrationsIds.map((id, index) => ({
value: id,
'data-test-subj': `migrationSelectionOption-${index}`,
label: i18n.SIEM_MIGRATIONS_OPTION_LABEL(index + 1),
}));
return options;
}, [migrationsIds]);
const selectedMigrationOption = useMemo<Array<EuiComboBoxOptionOption<string>>>(() => {
const index = migrationsIds.findIndex((id) => id === selectedMigrationId);
return index !== -1
? [
{
value: selectedMigrationId,
'data-test-subj': `migrationSelectionOption-${index}`,
label: i18n.SIEM_MIGRATIONS_OPTION_LABEL(index + 1),
},
]
: [];
}, [migrationsIds, selectedMigrationId]);

const onChange = (selected: Array<EuiComboBoxOptionOption<string>>) => {
onMigrationIdChange(selected[0].value);
};

if (!migrationsIds.length) {
return null;
}

return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
<EuiFlexItem grow={false}>
<EuiComboBox
aria-label={i18n.SIEM_MIGRATIONS_OPTION_AREAL_LABEL}
onChange={onChange}
options={migrationOptions}
selectedOptions={selectedMigrationOption}
singleSelection={{ asPlainText: true }}
isClearable={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { SecurityPageName } from '../../../../../common';
import { useGetSecuritySolutionLinkProps } from '../../../../common/components/links';
import * as i18n from './translations';

const SiemMigrationsTableNoItemsMessageComponent = () => {
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
const { onClick: onClickLink } = getSecuritySolutionLinkProps({
deepLinkId: SecurityPageName.rules,
});

return (
<EuiFlexGroup
alignItems="center"
gutterSize="s"
responsive={false}
direction="column"
wrap={true}
>
<EuiFlexItem grow={false}>
<EuiEmptyPrompt
title={<h2>{i18n.NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL}</h2>}
titleSize="s"
body={i18n.NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL_BODY}
data-test-subj="noRulesTranslationAvailableForInstall"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="arrowLeft"
color={'primary'}
onClick={onClickLink}
data-test-subj="rulesTranslationGoBackToRulesTableBtn"
>
{i18n.GO_BACK_TO_RULES_TABLE_BUTTON}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};

export const SiemMigrationsTableNoItemsMessage = React.memo(
SiemMigrationsTableNoItemsMessageComponent
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import {
EuiInMemoryTable,
EuiSkeletonLoading,
EuiProgress,
EuiSkeletonTitle,
EuiSkeletonText,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import React from 'react';

import {
RULES_TABLE_INITIAL_PAGE_SIZE,
RULES_TABLE_PAGE_SIZE_OPTIONS,
} from '../rules_table/constants';
import { SiemMigrationsTableNoItemsMessage } from './siem_migrations_no_items_message';
import { useSiemMigrationsTableContext } from './siem_migrations_table_context';
import { SiemMigrationsTableFilters } from './siem_migrations_table_filters';
import { useSiemMigrationsTableColumns } from './use_siem_migrations_table_columns';

/**
* Table Component for displaying SIEM rules migrations
*/
export const SiemMigrationsTable = React.memo(() => {
const siemMigrationsTableContext = useSiemMigrationsTableContext();

const {
state: { ruleMigrations, isLoading, selectedRuleMigrations },
actions: { selectRuleMigrations },
} = siemMigrationsTableContext;
const rulesColumns = useSiemMigrationsTableColumns();

const shouldShowProgress = isLoading;

return (
<>
{shouldShowProgress && (
<EuiProgress
data-test-subj="loadingRulesInfoProgress"
size="xs"
position="absolute"
color="accent"
/>
)}
<EuiSkeletonLoading
isLoading={isLoading}
loadingContent={
<>
<EuiSkeletonTitle />
<EuiSkeletonText />
</>
}
loadedContent={
!ruleMigrations.length ? (
<SiemMigrationsTableNoItemsMessage />
) : (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<SiemMigrationsTableFilters />
</EuiFlexItem>
</EuiFlexGroup>

<EuiInMemoryTable
items={ruleMigrations}
sorting
pagination={{
initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE,
pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS,
}}
selection={{
selectable: () => true,
onSelectionChange: selectRuleMigrations,
initialSelected: selectedRuleMigrations,
}}
itemId="rule_id"
data-test-subj="rules-translation-table"
columns={rulesColumns}
/>
</>
)
}
/>
</>
);
});

SiemMigrationsTable.displayName = 'SiemMigrationsTable';
Loading