Skip to content

Commit

Permalink
[Rules migration][UI] Basic rule migrations UI (#10820)
Browse files Browse the repository at this point in the history
  • Loading branch information
e40pud committed Nov 20, 2024
1 parent f6ac2cf commit 438de58
Show file tree
Hide file tree
Showing 31 changed files with 1,699 additions and 28 deletions.
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

0 comments on commit 438de58

Please sign in to comment.