mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
## Summary [Internal link](https://github.com/elastic/security-team/issues/10820) to the feature details This is a very first version of the SIEM rules migrations UI functionality. The main goal is to setup and agree on a folder structure where the feature gonna live. Tests covering feature will follow in a separate PR (see [internal link](https://github.com/elastic/security-team/issues/11232) for more details). The code follows the structure of prebuilt rules feature https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table and hidden behind `siemMigrationsEnabled` feature flag. ### Key UI changes * New "SIEM Rules Migrations." rules management sub-page * Navigation between different "finished" migrations * InMemory table with all the translations within the selected migration * Translation details preview flyout with `Translation` and `Overview` tabs * User cannot modify translations via UI ### Testing locally Enable the flag ``` xpack.securitySolution.enableExperimental: ['siemMigrationsEnabled'] ``` ### Screenshot https://github.com/user-attachments/assets/a5a7e777-c5f8-40b4-be1d-1bd07a2729ac
This commit is contained in:
parent
556edb9971
commit
a627e011a8
42 changed files with 1705 additions and 0 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -2098,6 +2098,7 @@ x-pack/test/security_solution_api_integration/test_suites/sources @elastic/secur
|
|||
|
||||
/x-pack/plugins/security_solution/server/lib/siem_migrations @elastic/security-threat-hunting
|
||||
/x-pack/plugins/security_solution/common/siem_migrations @elastic/security-threat-hunting
|
||||
/x-pack/plugins/security_solution/public/siem_migrations @elastic/security-threat-hunting
|
||||
|
||||
## Security Solution Threat Hunting areas - Threat Hunting Investigations
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ export enum SecurityPageName {
|
|||
rulesAdd = 'rules-add',
|
||||
rulesCreate = 'rules-create',
|
||||
rulesLanding = 'rules-landing',
|
||||
siemMigrationsRules = 'siem_migrations-rules',
|
||||
/*
|
||||
* 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
|
||||
|
|
|
@ -138,6 +138,8 @@ export const APP_BLOCKLIST_PATH = `${APP_PATH}${BLOCKLIST_PATH}` as const;
|
|||
export const APP_RESPONSE_ACTIONS_HISTORY_PATH =
|
||||
`${APP_PATH}${RESPONSE_ACTIONS_HISTORY_PATH}` as const;
|
||||
export const NOTES_PATH = `${MANAGEMENT_PATH}/notes` as const;
|
||||
export const SIEM_MIGRATIONS_PATH = '/siem_migrations' as const;
|
||||
export const SIEM_MIGRATIONS_RULES_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const;
|
||||
|
||||
// cloud logs to exclude from default index pattern
|
||||
export const EXCLUDE_ELASTIC_CLOUD_INDICES = ['-*elastic-cloud-logs-*'];
|
||||
|
|
|
@ -101,6 +101,13 @@ export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exce
|
|||
defaultMessage: 'Shared exception lists',
|
||||
});
|
||||
|
||||
export const SIEM_MIGRATIONS_RULES = i18n.translate(
|
||||
'xpack.securitySolution.navigation.siemMigrationsRules',
|
||||
{
|
||||
defaultMessage: 'SIEM Rules Migrations',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', {
|
||||
defaultMessage: 'Alerts',
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ import { EntityAnalytics } from './entity_analytics';
|
|||
import { Assets } from './assets';
|
||||
import { Investigations } from './investigations';
|
||||
import { MachineLearning } from './machine_learning';
|
||||
import { SiemMigrations } from './siem_migrations';
|
||||
|
||||
/**
|
||||
* The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import.
|
||||
|
@ -53,5 +54,6 @@ const subPluginClasses = {
|
|||
Assets,
|
||||
Investigations,
|
||||
MachineLearning,
|
||||
SiemMigrations,
|
||||
};
|
||||
export { subPluginClasses };
|
||||
|
|
|
@ -245,6 +245,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
assets: new subPluginClasses.Assets(),
|
||||
investigations: new subPluginClasses.Investigations(),
|
||||
machineLearning: new subPluginClasses.MachineLearning(),
|
||||
siemMigrations: new subPluginClasses.SiemMigrations(),
|
||||
};
|
||||
}
|
||||
return this._subPlugins;
|
||||
|
@ -279,6 +280,9 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
assets: subPlugins.assets.start(),
|
||||
investigations: subPlugins.investigations.start(),
|
||||
machineLearning: subPlugins.machineLearning.start(),
|
||||
siemMigrations: subPlugins.siemMigrations.start(
|
||||
this.experimentalFeatures.siemMigrationsEnabled
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import type { LinkItem } from '../common/links';
|
|||
import { IconConsoleCloud } from '../common/icons/console_cloud';
|
||||
import { IconRollup } from '../common/icons/rollup';
|
||||
import { IconDashboards } from '../common/icons/dashboards';
|
||||
import { siemMigrationsLinks } from '../siem_migrations/links';
|
||||
|
||||
export const links: LinkItem = {
|
||||
id: SecurityPageName.rulesLanding,
|
||||
|
@ -106,6 +107,7 @@ export const links: LinkItem = {
|
|||
}),
|
||||
],
|
||||
},
|
||||
siemMigrationsLinks,
|
||||
],
|
||||
categories: [
|
||||
{
|
||||
|
@ -116,6 +118,7 @@ export const links: LinkItem = {
|
|||
SecurityPageName.rules,
|
||||
SecurityPageName.cloudSecurityPostureBenchmarks,
|
||||
SecurityPageName.exceptions,
|
||||
SecurityPageName.siemMigrationsRules,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { SecuritySubPlugin } from '../app/types';
|
||||
import { routes } from './routes';
|
||||
|
||||
export class SiemMigrations {
|
||||
public setup() {}
|
||||
|
||||
public start(isEnabled = false): SecuritySubPlugin {
|
||||
return {
|
||||
routes: isEnabled ? routes : [],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/security_solution/public/siem_migrations'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/siem_migrations',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/security_solution/public/siem_migrations/**/*.{ts,tsx}',
|
||||
],
|
||||
moduleNameMapper: require('../../server/__mocks__/module_name_map'),
|
||||
};
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
SecurityPageName,
|
||||
SERVER_APP_ID,
|
||||
SIEM_MIGRATIONS_RULES_PATH,
|
||||
} from '../../common/constants';
|
||||
import { SIEM_MIGRATIONS_RULES } from '../app/translations';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
import { IconConsoleCloud } from '../common/icons/console_cloud';
|
||||
|
||||
export const siemMigrationsLinks: LinkItem = {
|
||||
id: SecurityPageName.siemMigrationsRules,
|
||||
title: SIEM_MIGRATIONS_RULES,
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRulesDescription', {
|
||||
defaultMessage: 'SIEM Rules Migrations.',
|
||||
}),
|
||||
landingIcon: IconConsoleCloud,
|
||||
path: SIEM_MIGRATIONS_RULES_PATH,
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRules', {
|
||||
defaultMessage: 'SIEM Rules Migrations',
|
||||
}),
|
||||
],
|
||||
experimentalKey: 'siemMigrationsEnabled',
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 type { SecuritySubPluginRoutes } from '../app/types';
|
||||
import { SIEM_MIGRATIONS_RULES_PATH, SecurityPageName } from '../../common/constants';
|
||||
import { RulesPage } from './rules/pages';
|
||||
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
|
||||
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
|
||||
|
||||
export const RulesRoutes = () => {
|
||||
return (
|
||||
<PluginTemplateWrapper>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.siemMigrationsRules}>
|
||||
<RulesPage />
|
||||
</SecurityRoutePageWrapper>
|
||||
</PluginTemplateWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const routes: SecuritySubPluginRoutes = [
|
||||
{
|
||||
path: SIEM_MIGRATIONS_RULES_PATH,
|
||||
component: RulesRoutes,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { replaceParams } from '@kbn/openapi-common/shared';
|
||||
|
||||
import { KibanaServices } from '../../../common/lib/kibana';
|
||||
|
||||
import {
|
||||
SIEM_RULE_MIGRATIONS_ALL_STATS_PATH,
|
||||
SIEM_RULE_MIGRATION_PATH,
|
||||
} from '../../../../common/siem_migrations/constants';
|
||||
import type {
|
||||
GetAllStatsRuleMigrationResponse,
|
||||
GetRuleMigrationResponse,
|
||||
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
|
||||
|
||||
/**
|
||||
* 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> => {
|
||||
return 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> => {
|
||||
return KibanaServices.get().http.fetch<GetRuleMigrationResponse>(
|
||||
replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }),
|
||||
{
|
||||
method: 'GET',
|
||||
version: '1',
|
||||
signal,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const ONE_MINUTE = 60000;
|
||||
|
||||
export const DEFAULT_QUERY_OPTIONS = {
|
||||
refetchIntervalInBackground: false,
|
||||
staleTime: ONE_MINUTE * 5,
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { DEFAULT_QUERY_OPTIONS } from './constants';
|
||||
import { getRuleMigrations } from '../api';
|
||||
import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
|
||||
import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants';
|
||||
|
||||
export const useGetRuleMigrationsQuery = (
|
||||
migrationId: string,
|
||||
options?: UseQueryOptions<GetRuleMigrationResponse>
|
||||
) => {
|
||||
const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, {
|
||||
migration_id: migrationId,
|
||||
});
|
||||
return useQuery<GetRuleMigrationResponse>(
|
||||
['GET', SPECIFIC_MIGRATION_PATH],
|
||||
async ({ signal }) => {
|
||||
return getRuleMigrations({ migrationId, signal });
|
||||
},
|
||||
{
|
||||
...DEFAULT_QUERY_OPTIONS,
|
||||
...options,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 { DEFAULT_QUERY_OPTIONS } from './constants';
|
||||
import { getRuleMigrationsStatsAll } from '../api';
|
||||
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';
|
||||
|
||||
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,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface HeaderButtonsProps {
|
||||
/**
|
||||
* Available rule migrations ids
|
||||
*/
|
||||
migrationsIds: string[];
|
||||
|
||||
/**
|
||||
* Selected rule migration id
|
||||
*/
|
||||
selectedMigrationId: string | undefined;
|
||||
|
||||
/**
|
||||
* Handles migration selection changes
|
||||
* @param selectedId Selected migration id
|
||||
* @returns
|
||||
*/
|
||||
onMigrationIdChange: (selectedId?: string) => void;
|
||||
}
|
||||
|
||||
const HeaderButtonsComponent: React.FC<HeaderButtonsProps> = ({
|
||||
migrationsIds,
|
||||
selectedMigrationId,
|
||||
onMigrationIdChange,
|
||||
}) => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export const HeaderButtons = React.memo(HeaderButtonsComponent);
|
||||
HeaderButtons.displayName = 'HeaderButtons';
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SIEM_MIGRATIONS_OPTION_AREAL_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.selectionOption.arealLabel',
|
||||
{
|
||||
defaultMessage: 'Select a migration',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIEM_MIGRATIONS_OPTION_LABEL = (optionIndex: number) =>
|
||||
i18n.translate('xpack.securitySolution.siemMigrations.rules.selectionOption.title', {
|
||||
defaultMessage: 'SIEM rule migration {optionIndex}',
|
||||
values: {
|
||||
optionIndex,
|
||||
},
|
||||
});
|
|
@ -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 NoMigrationsComponent = () => {
|
||||
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
|
||||
const { onClick: onClickLink } = getSecuritySolutionLinkProps({
|
||||
deepLinkId: SecurityPageName.landing,
|
||||
path: 'siem_migrations',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
direction="column"
|
||||
wrap={true}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>{i18n.NO_MIGRATIONS_AVAILABLE}</h2>}
|
||||
titleSize="s"
|
||||
body={i18n.NO_MIGRATIONS_AVAILABLE_BODY}
|
||||
data-test-subj="noMigrationsAvailable"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="arrowLeft"
|
||||
color={'primary'}
|
||||
onClick={onClickLink}
|
||||
data-test-subj="goToSiemMigrationsButton"
|
||||
>
|
||||
{i18n.GO_BACK_TO_RULES_TABLE_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoMigrations = React.memo(NoMigrationsComponent);
|
||||
NoMigrations.displayName = 'NoMigrations';
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const NO_MIGRATIONS_AVAILABLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.noMigrationsTitle',
|
||||
{
|
||||
defaultMessage: 'No migrations',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_MIGRATIONS_AVAILABLE_BODY = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.noMigrationsBodyTitle',
|
||||
{
|
||||
defaultMessage: 'There are no migrations available',
|
||||
}
|
||||
);
|
||||
|
||||
export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.goToMigrationsPageButton',
|
||||
{
|
||||
defaultMessage: 'Go back to SIEM Migrations',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup } from '@elastic/eui';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import * as i18n from './translations';
|
||||
import { RuleSearchField } from '../../../../detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_search_field';
|
||||
import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install';
|
||||
|
||||
const FilterWrapper = styled(EuiFlexGroup)`
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeM};
|
||||
`;
|
||||
|
||||
export interface FiltersComponentProps {
|
||||
/**
|
||||
* Currently selected table filter
|
||||
*/
|
||||
filterOptions: TableFilterOptions;
|
||||
|
||||
/**
|
||||
* Handles filter options changes
|
||||
*/
|
||||
setFilterOptions: Dispatch<SetStateAction<TableFilterOptions>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of filters for filtering data within the SIEM Rules Migrations table.
|
||||
* Contains search bar and tag selection
|
||||
*/
|
||||
const FiltersComponent: React.FC<FiltersComponentProps> = ({ filterOptions, setFilterOptions }) => {
|
||||
const handleOnSearch = useCallback(
|
||||
(filterString: string) => {
|
||||
setFilterOptions((filters) => ({
|
||||
...filters,
|
||||
filter: filterString.trim(),
|
||||
}));
|
||||
},
|
||||
[setFilterOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterWrapper gutterSize="m" justifyContent="flexEnd" wrap>
|
||||
<RuleSearchField
|
||||
initialValue={filterOptions.filter}
|
||||
onSearch={handleOnSearch}
|
||||
placeholder={i18n.SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</FilterWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const Filters = React.memo(FiltersComponent);
|
||||
Filters.displayName = 'Filters';
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import {
|
||||
RULES_TABLE_INITIAL_PAGE_SIZE,
|
||||
RULES_TABLE_PAGE_SIZE_OPTIONS,
|
||||
} from '../../../../detection_engine/rule_management_ui/components/rules_table/constants';
|
||||
import { NoItemsMessage } from './no_items_message';
|
||||
import { Filters } from './filters';
|
||||
import { useRulesTableColumns } from '../../hooks/use_rules_table_columns';
|
||||
import { useGetRuleMigrationsQuery } from '../../api/hooks/use_get_rule_migrations';
|
||||
import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install';
|
||||
import { useFilterRulesToInstall } from '../../hooks/use_filter_rules_to_install';
|
||||
|
||||
export interface RulesTableComponentProps {
|
||||
/**
|
||||
* Selected rule migration id
|
||||
*/
|
||||
migrationId: string;
|
||||
|
||||
/**
|
||||
* Opens the flyout with the details of the rule migration
|
||||
* @param rule Rule migration
|
||||
* @returns
|
||||
*/
|
||||
openRulePreview: (rule: RuleMigration) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table Component for displaying SIEM rules migrations
|
||||
*/
|
||||
const RulesTableComponent: React.FC<RulesTableComponentProps> = ({
|
||||
migrationId,
|
||||
openRulePreview,
|
||||
}) => {
|
||||
const { data: ruleMigrations, isLoading } = useGetRuleMigrationsQuery(migrationId);
|
||||
|
||||
const [selectedRuleMigrations, setSelectedRuleMigrations] = useState<RuleMigration[]>([]);
|
||||
|
||||
const [filterOptions, setFilterOptions] = useState<TableFilterOptions>({
|
||||
filter: '',
|
||||
});
|
||||
|
||||
const filteredRuleMigrations = useFilterRulesToInstall({
|
||||
filterOptions,
|
||||
ruleMigrations: ruleMigrations ?? [],
|
||||
});
|
||||
|
||||
const shouldShowProgress = isLoading;
|
||||
|
||||
const rulesColumns = useRulesTableColumns({
|
||||
openRulePreview,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowProgress && (
|
||||
<EuiProgress
|
||||
data-test-subj="loadingRulesInfoProgress"
|
||||
size="xs"
|
||||
position="absolute"
|
||||
color="accent"
|
||||
/>
|
||||
)}
|
||||
<EuiSkeletonLoading
|
||||
isLoading={isLoading}
|
||||
loadingContent={
|
||||
<>
|
||||
<EuiSkeletonTitle />
|
||||
<EuiSkeletonText />
|
||||
</>
|
||||
}
|
||||
loadedContent={
|
||||
!filteredRuleMigrations.length ? (
|
||||
<NoItemsMessage />
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<Filters filterOptions={filterOptions} setFilterOptions={setFilterOptions} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiInMemoryTable
|
||||
items={filteredRuleMigrations}
|
||||
sorting
|
||||
pagination={{
|
||||
initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE,
|
||||
pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS,
|
||||
}}
|
||||
selection={{
|
||||
selectable: () => true,
|
||||
onSelectionChange: setSelectedRuleMigrations,
|
||||
initialSelected: selectedRuleMigrations,
|
||||
}}
|
||||
itemId="rule_id"
|
||||
data-test-subj="rules-translation-table"
|
||||
columns={rulesColumns}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RulesTable = React.memo(RulesTableComponent);
|
||||
RulesTable.displayName = 'RulesTable';
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 NoItemsMessageComponent = () => {
|
||||
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
|
||||
const { onClick: onClickLink } = getSecuritySolutionLinkProps({
|
||||
deepLinkId: SecurityPageName.landing,
|
||||
path: 'siem_migrations',
|
||||
});
|
||||
|
||||
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="goToSiemMigrationsButton"
|
||||
>
|
||||
{i18n.GO_BACK_TO_RULES_TABLE_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoItemsMessage = React.memo(NoItemsMessageComponent);
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.searchBarPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search by rule name',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.noRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Empty migration',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL_BODY = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.noRulesBodyTitle',
|
||||
{
|
||||
defaultMessage: 'There are no translations available for installation',
|
||||
}
|
||||
);
|
||||
|
||||
export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.goToMigrationsPageButton',
|
||||
{
|
||||
defaultMessage: 'Go back to SIEM Migrations',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
|
||||
import { StatusBadge } from '.';
|
||||
|
||||
describe('StatusBadge', () => {
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(<StatusBadge value="full" />);
|
||||
|
||||
expect(wrapper.find('HealthTruncateText')).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { euiLightVars } from '@kbn/ui-theme';
|
||||
|
||||
import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { HealthTruncateText } from '../../../../common/components/health_truncate_text';
|
||||
import { convertTranslationResultIntoText } from '../../utils/helpers';
|
||||
|
||||
const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars;
|
||||
const statusToColorMap: Record<RuleMigrationTranslationResult, string> = {
|
||||
full: euiColorVis0,
|
||||
partial: euiColorVis7,
|
||||
untranslatable: euiColorVis9,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
value?: RuleMigrationTranslationResult;
|
||||
installedRuleId?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
const StatusBadgeComponent: React.FC<Props> = ({
|
||||
value,
|
||||
installedRuleId,
|
||||
'data-test-subj': dataTestSubj = 'translation-result',
|
||||
}) => {
|
||||
const translationResult = installedRuleId || !value ? 'full' : value;
|
||||
const displayValue = convertTranslationResultIntoText(translationResult);
|
||||
const color = statusToColorMap[translationResult];
|
||||
|
||||
return (
|
||||
<HealthTruncateText
|
||||
healthColor={color}
|
||||
tooltipContent={displayValue}
|
||||
dataTestSubj={dataTestSubj}
|
||||
>
|
||||
{displayValue}
|
||||
</HealthTruncateText>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusBadge = React.memo(StatusBadgeComponent);
|
||||
StatusBadge.displayName = 'StatusBadge';
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['50%', '50%'];
|
||||
export const LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['30%', '70%'];
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
* 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 { FC, PropsWithChildren } from 'react';
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { css } from '@emotion/css';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiTitle,
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiTabbedContent,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui';
|
||||
|
||||
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import {
|
||||
RuleOverviewTab,
|
||||
useOverviewTabSections,
|
||||
} from '../../../../detection_engine/rule_management/components/rule_details/rule_overview_tab';
|
||||
import {
|
||||
type RuleResponse,
|
||||
type Severity,
|
||||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import {
|
||||
DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS,
|
||||
LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS,
|
||||
} from './constants';
|
||||
import { TranslationTab } from './translation_tab';
|
||||
import {
|
||||
DEFAULT_TRANSLATION_RISK_SCORE,
|
||||
DEFAULT_TRANSLATION_SEVERITY,
|
||||
} from '../../utils/constants';
|
||||
|
||||
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
||||
.euiFlyoutBody__overflow {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.euiFlyoutBody__overflowContent {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: ${({ theme }) => `0 ${theme.eui.euiSizeL} 0`};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFlexGroup = styled(EuiFlexGroup)`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledEuiFlexItem = styled(EuiFlexItem)`
|
||||
&.euiFlexItem {
|
||||
flex: 1 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEuiTabbedContent = styled(EuiTabbedContent)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
> [role='tabpanel'] {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/*
|
||||
* Fixes tabs to the top and allows the content to scroll.
|
||||
*/
|
||||
const ScrollableFlyoutTabbedContent = (props: EuiTabbedContentProps) => (
|
||||
<StyledFlexGroup direction="column" gutterSize="none">
|
||||
<StyledEuiFlexItem grow={true}>
|
||||
<StyledEuiTabbedContent {...props} />
|
||||
</StyledEuiFlexItem>
|
||||
</StyledFlexGroup>
|
||||
);
|
||||
|
||||
const tabPaddingClassName = css`
|
||||
padding: 0 ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeXL} ${euiThemeVars.euiSizeM};
|
||||
`;
|
||||
|
||||
export const TabContentPadding: FC<PropsWithChildren<unknown>> = ({ children }) => (
|
||||
<div className={tabPaddingClassName}>{children}</div>
|
||||
);
|
||||
|
||||
interface TranslationDetailsFlyoutProps {
|
||||
ruleActions?: React.ReactNode;
|
||||
ruleMigration: RuleMigration;
|
||||
size?: EuiFlyoutProps['size'];
|
||||
extraTabs?: EuiTabbedContentTab[];
|
||||
closeFlyout: () => void;
|
||||
}
|
||||
|
||||
export const TranslationDetailsFlyout = ({
|
||||
ruleActions,
|
||||
ruleMigration,
|
||||
size = 'm',
|
||||
extraTabs = [],
|
||||
closeFlyout,
|
||||
}: TranslationDetailsFlyoutProps) => {
|
||||
const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections();
|
||||
|
||||
const rule: RuleResponse = useMemo(() => {
|
||||
const esqlLanguage = ruleMigration.elastic_rule?.query_language ?? 'esql';
|
||||
return {
|
||||
type: esqlLanguage,
|
||||
language: esqlLanguage,
|
||||
name: ruleMigration.elastic_rule?.title,
|
||||
description: ruleMigration.elastic_rule?.description,
|
||||
query: ruleMigration.elastic_rule?.query,
|
||||
|
||||
// Default values
|
||||
severity: (ruleMigration.elastic_rule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY,
|
||||
risk_score: DEFAULT_TRANSLATION_RISK_SCORE,
|
||||
from: 'now-360s',
|
||||
to: 'now',
|
||||
interval: '5m',
|
||||
} as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter
|
||||
}, [ruleMigration]);
|
||||
|
||||
const translationTab: EuiTabbedContentTab = useMemo(
|
||||
() => ({
|
||||
id: 'translation',
|
||||
name: i18n.TRANSLATION_TAB_LABEL,
|
||||
content: (
|
||||
<TabContentPadding>
|
||||
<TranslationTab ruleMigration={ruleMigration} />
|
||||
</TabContentPadding>
|
||||
),
|
||||
}),
|
||||
[ruleMigration]
|
||||
);
|
||||
|
||||
const overviewTab: EuiTabbedContentTab = useMemo(
|
||||
() => ({
|
||||
id: 'overview',
|
||||
name: i18n.OVERVIEW_TAB_LABEL,
|
||||
content: (
|
||||
<TabContentPadding>
|
||||
<RuleOverviewTab
|
||||
rule={rule}
|
||||
columnWidths={
|
||||
size === 'l'
|
||||
? LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS
|
||||
: DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS
|
||||
}
|
||||
expandedOverviewSections={expandedOverviewSections}
|
||||
toggleOverviewSection={toggleOverviewSection}
|
||||
/>
|
||||
</TabContentPadding>
|
||||
),
|
||||
}),
|
||||
[rule, size, expandedOverviewSections, toggleOverviewSection]
|
||||
);
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
return [...extraTabs, translationTab, overviewTab];
|
||||
}, [extraTabs, translationTab, overviewTab]);
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState<string>(tabs[0].id);
|
||||
const selectedTab = tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0];
|
||||
|
||||
useEffect(() => {
|
||||
if (!tabs.find((tab) => tab.id === selectedTabId)) {
|
||||
// Switch to first tab if currently selected tab is not available for this rule
|
||||
setSelectedTabId(tabs[0].id);
|
||||
}
|
||||
}, [tabs, selectedTabId]);
|
||||
|
||||
const onTabClick = (tab: EuiTabbedContentTab) => {
|
||||
setSelectedTabId(tab.id);
|
||||
};
|
||||
|
||||
const migrationsRulesFlyoutTitleId = useGeneratedHtmlId({
|
||||
prefix: 'migrationRulesFlyoutTitle',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
size={size}
|
||||
onClose={closeFlyout}
|
||||
key="migrations-rules-flyout"
|
||||
paddingSize="l"
|
||||
data-test-subj="ruleMigrationDetailsFlyout"
|
||||
aria-labelledby={migrationsRulesFlyoutTitleId}
|
||||
ownFocus
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id={migrationsRulesFlyoutTitleId}>{rule.name}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
</EuiFlyoutHeader>
|
||||
<StyledEuiFlyoutBody>
|
||||
<ScrollableFlyoutTabbedContent
|
||||
tabs={tabs}
|
||||
selectedTab={selectedTab}
|
||||
onTabClick={onTabClick}
|
||||
/>
|
||||
</StyledEuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={closeFlyout} flush="left">
|
||||
{i18n.DISMISS_BUTTON_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{ruleActions}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiTitle } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export function TranslationTabHeader(): JSX.Element {
|
||||
return (
|
||||
<EuiFlexGroup direction="row" alignItems="center">
|
||||
<EuiTitle data-test-subj="ruleTranslationLabel" size="xs">
|
||||
<h5>{i18n.TAB_HEADER_TITLE}</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiAccordion,
|
||||
EuiBadge,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiSplitPanel,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { TranslationTabHeader } from './header';
|
||||
import { RuleQueryComponent } from './rule_query';
|
||||
import * as i18n from './translations';
|
||||
import { convertTranslationResultIntoText } from '../../../utils/helpers';
|
||||
|
||||
interface TranslationTabProps {
|
||||
ruleMigration: RuleMigration;
|
||||
}
|
||||
|
||||
export const TranslationTab = ({ ruleMigration }: TranslationTabProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const name = ruleMigration.elastic_rule?.title ?? ruleMigration.original_rule.title;
|
||||
const originalQuery = ruleMigration.original_rule.query;
|
||||
const elasticQuery = ruleMigration.elastic_rule?.query ?? 'Prebuilt rule query';
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow label={i18n.NAME_LABEL} fullWidth>
|
||||
<EuiFieldText value={name} fullWidth />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiAccordion
|
||||
id="translationQueryItem"
|
||||
buttonContent={<TranslationTabHeader />}
|
||||
initialIsOpen={true}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSplitPanel.Outer grow hasShadow={false} hasBorder={true}>
|
||||
<EuiSplitPanel.Inner grow={false} color="subdued" paddingSize="s">
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.translationDetails.translationTab.statusTitle"
|
||||
defaultMessage="Translation status"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
color="primary"
|
||||
onClick={() => {}}
|
||||
onClickAriaLabel={'Click to update translation status'}
|
||||
>
|
||||
{convertTranslationResultIntoText(ruleMigration.translation_result)}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner grow>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
|
||||
<EuiFlexItem grow={1}>
|
||||
<RuleQueryComponent
|
||||
title={i18n.SPLUNK_QUERY_TITLE}
|
||||
query={originalQuery}
|
||||
canEdit={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={0}
|
||||
css={css`
|
||||
align-self: stretch;
|
||||
border-right: ${euiTheme.border.thin};
|
||||
`}
|
||||
/>
|
||||
<EuiFlexItem grow={1}>
|
||||
<RuleQueryComponent
|
||||
title={i18n.ESQL_TRANSLATION_TITLE}
|
||||
query={elasticQuery}
|
||||
canEdit={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiFlexItem>
|
||||
</EuiAccordion>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { EuiMarkdownEditor, EuiMarkdownFormat, EuiTitle } from '@elastic/eui';
|
||||
import { SideHeader } from '../../../../../detection_engine/rule_management/components/rule_details/three_way_diff/components/side_header';
|
||||
import { FinalSideHelpInfo } from '../../../../../detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface RuleQueryProps {
|
||||
title: string;
|
||||
query: string;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
|
||||
export const RuleQueryComponent = ({ title, query, canEdit }: RuleQueryProps) => {
|
||||
const queryTextComponent = useMemo(() => {
|
||||
if (canEdit) {
|
||||
return (
|
||||
<EuiMarkdownEditor
|
||||
aria-label={i18n.TRANSLATED_QUERY_AREAL_LABEL}
|
||||
value={query}
|
||||
onChange={() => {}}
|
||||
height={400}
|
||||
initialViewMode={'viewing'}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <EuiMarkdownFormat>{query}</EuiMarkdownFormat>;
|
||||
}
|
||||
}, [canEdit, query]);
|
||||
return (
|
||||
<>
|
||||
<SideHeader>
|
||||
<EuiTitle size="xxs">
|
||||
<h3>
|
||||
{title}
|
||||
<FinalSideHelpInfo />
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</SideHeader>
|
||||
{queryTextComponent}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const TAB_HEADER_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.title',
|
||||
{
|
||||
defaultMessage: 'Translation',
|
||||
}
|
||||
);
|
||||
|
||||
export const NAME_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.nameLabel',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
||||
|
||||
export const SPLUNK_QUERY_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.splunkQueryTitle',
|
||||
{
|
||||
defaultMessage: 'Splunk query',
|
||||
}
|
||||
);
|
||||
|
||||
export const ESQL_TRANSLATION_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.esqlTranslationTitle',
|
||||
{
|
||||
defaultMessage: 'ES|QL translation',
|
||||
}
|
||||
);
|
||||
|
||||
export const TRANSLATED_QUERY_AREAL_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.queryArealLabel',
|
||||
{
|
||||
defaultMessage: 'Translated query',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const OVERVIEW_TAB_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationDetails.overviewTabLabel',
|
||||
{
|
||||
defaultMessage: 'Overview',
|
||||
}
|
||||
);
|
||||
|
||||
export const TRANSLATION_TAB_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTabLabel',
|
||||
{
|
||||
defaultMessage: 'Translation',
|
||||
}
|
||||
);
|
||||
|
||||
export const DISMISS_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationDetails.dismissButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Dismiss',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const COLUMN_STATUS = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.columns.statusTitle',
|
||||
{
|
||||
defaultMessage: 'Status',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import type { FilterOptions } from '../../../detection_engine/rule_management/logic/types';
|
||||
|
||||
export type TableFilterOptions = Pick<FilterOptions, 'filter'>;
|
||||
|
||||
export const useFilterRulesToInstall = ({
|
||||
ruleMigrations,
|
||||
filterOptions,
|
||||
}: {
|
||||
ruleMigrations: RuleMigration[];
|
||||
filterOptions: TableFilterOptions;
|
||||
}) => {
|
||||
const filteredRules = useMemo(() => {
|
||||
const { filter } = filterOptions;
|
||||
return ruleMigrations.filter((migration) => {
|
||||
const name = migration.elastic_rule?.title ?? migration.original_rule.title;
|
||||
if (filter && !name.toLowerCase().includes(filter.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [filterOptions, ruleMigrations]);
|
||||
|
||||
return filteredRules;
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { ReactNode } from 'react';
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import type { EuiTabbedContentTab } from '@elastic/eui';
|
||||
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { TranslationDetailsFlyout } from '../components/translation_details_flyout';
|
||||
|
||||
interface UseRulePreviewFlyoutParams {
|
||||
ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode;
|
||||
extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[];
|
||||
}
|
||||
|
||||
interface UseRulePreviewFlyoutResult {
|
||||
rulePreviewFlyout: ReactNode;
|
||||
openRulePreview: (rule: RuleMigration) => void;
|
||||
closeRulePreview: () => void;
|
||||
}
|
||||
|
||||
export function useRulePreviewFlyout({
|
||||
extraTabsFactory,
|
||||
ruleActionsFactory,
|
||||
}: UseRulePreviewFlyoutParams): UseRulePreviewFlyoutResult {
|
||||
const [ruleMigration, setRuleMigrationForPreview] = useState<RuleMigration | undefined>();
|
||||
const closeRulePreview = useCallback(() => setRuleMigrationForPreview(undefined), []);
|
||||
const ruleActions = useMemo(
|
||||
() => ruleMigration && ruleActionsFactory(ruleMigration, closeRulePreview),
|
||||
[ruleMigration, ruleActionsFactory, closeRulePreview]
|
||||
);
|
||||
const extraTabs = useMemo(
|
||||
() => (ruleMigration && extraTabsFactory ? extraTabsFactory(ruleMigration) : []),
|
||||
[ruleMigration, extraTabsFactory]
|
||||
);
|
||||
|
||||
return {
|
||||
rulePreviewFlyout: ruleMigration && (
|
||||
<TranslationDetailsFlyout
|
||||
ruleMigration={ruleMigration}
|
||||
size="l"
|
||||
closeFlyout={closeRulePreview}
|
||||
ruleActions={ruleActions}
|
||||
extraTabs={extraTabs}
|
||||
/>
|
||||
),
|
||||
openRulePreview: useCallback((rule: RuleMigration) => {
|
||||
setRuleMigrationForPreview(rule);
|
||||
}, []),
|
||||
closeRulePreview,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiText, EuiLink } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { SeverityBadge } from '../../../common/components/severity_badge';
|
||||
import * as rulesI18n from '../../../detections/pages/detection_engine/rules/translations';
|
||||
import * as i18n from './translations';
|
||||
import { getNormalizedSeverity } from '../../../detection_engine/rule_management_ui/components/rules_table/helpers';
|
||||
import { StatusBadge } from '../components/status_badge';
|
||||
import { DEFAULT_TRANSLATION_RISK_SCORE, DEFAULT_TRANSLATION_SEVERITY } from '../utils/constants';
|
||||
|
||||
export type TableColumn = EuiBasicTableColumn<RuleMigration>;
|
||||
|
||||
interface RuleNameProps {
|
||||
name: string;
|
||||
rule: RuleMigration;
|
||||
openRulePreview: (rule: RuleMigration) => void;
|
||||
}
|
||||
|
||||
const RuleName = ({ name, rule, openRulePreview }: RuleNameProps) => {
|
||||
return (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
openRulePreview(rule);
|
||||
}}
|
||||
data-test-subj="ruleName"
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
const createRuleNameColumn = ({
|
||||
openRulePreview,
|
||||
}: {
|
||||
openRulePreview: (rule: RuleMigration) => void;
|
||||
}): TableColumn => {
|
||||
return {
|
||||
field: 'original_rule.title',
|
||||
name: rulesI18n.COLUMN_RULE,
|
||||
render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => (
|
||||
<RuleName name={value} rule={rule} openRulePreview={openRulePreview} />
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '40%',
|
||||
align: 'left',
|
||||
};
|
||||
};
|
||||
|
||||
const STATUS_COLUMN: TableColumn = {
|
||||
field: 'translation_result',
|
||||
name: i18n.COLUMN_STATUS,
|
||||
render: (value: RuleMigration['translation_result'], rule: RuleMigration) => (
|
||||
<StatusBadge value={value} installedRuleId={rule.elastic_rule?.id} />
|
||||
),
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
width: '12%',
|
||||
};
|
||||
|
||||
export const useRulesTableColumns = ({
|
||||
openRulePreview,
|
||||
}: {
|
||||
openRulePreview: (rule: RuleMigration) => void;
|
||||
}): TableColumn[] => {
|
||||
return useMemo(
|
||||
() => [
|
||||
createRuleNameColumn({ openRulePreview }),
|
||||
STATUS_COLUMN,
|
||||
{
|
||||
field: 'risk_score',
|
||||
name: rulesI18n.COLUMN_RISK_SCORE,
|
||||
render: () => (
|
||||
<EuiText data-test-subj="riskScore" size="s">
|
||||
{DEFAULT_TRANSLATION_RISK_SCORE}
|
||||
</EuiText>
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '75px',
|
||||
},
|
||||
{
|
||||
field: 'elastic_rule.severity',
|
||||
name: rulesI18n.COLUMN_SEVERITY,
|
||||
render: (value?: Severity) => (
|
||||
<SeverityBadge value={value ?? DEFAULT_TRANSLATION_SEVERITY} />
|
||||
),
|
||||
sortable: ({ elastic_rule: elasticRule }: RuleMigration) =>
|
||||
getNormalizedSeverity(
|
||||
(elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY
|
||||
),
|
||||
truncateText: true,
|
||||
width: '12%',
|
||||
},
|
||||
],
|
||||
[openRulePreview]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui';
|
||||
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { HeaderPage } from '../../../common/components/header_page';
|
||||
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
|
||||
import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { RulesTable } from '../components/rules_table';
|
||||
import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/callouts/need_admin_for_update_callout';
|
||||
import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout';
|
||||
import { HeaderButtons } from '../components/header_buttons';
|
||||
import { useGetRuleMigrationsStatsAllQuery } from '../api/hooks/use_get_rule_migrations_stats_all';
|
||||
import { useRulePreviewFlyout } from '../hooks/use_rule_preview_flyout';
|
||||
import { NoMigrations } from '../components/no_migrations';
|
||||
|
||||
const RulesPageComponent: React.FC = () => {
|
||||
const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } =
|
||||
useGetRuleMigrationsStatsAllQuery();
|
||||
|
||||
const migrationsIds = useMemo(() => {
|
||||
if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) {
|
||||
return [];
|
||||
}
|
||||
return ruleMigrationsStatsAll
|
||||
.filter((migration) => migration.status === 'finished')
|
||||
.map((migration) => migration.migration_id);
|
||||
}, [isLoadingMigrationsStats, ruleMigrationsStatsAll]);
|
||||
|
||||
const [selectedMigrationId, setSelectedMigrationId] = useState<string | undefined>();
|
||||
const onMigrationIdChange = (selectedId?: string) => {
|
||||
setSelectedMigrationId(selectedId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!migrationsIds.length) {
|
||||
return;
|
||||
}
|
||||
const index = migrationsIds.findIndex((id) => id === selectedMigrationId);
|
||||
if (index === -1) {
|
||||
setSelectedMigrationId(migrationsIds[0]);
|
||||
}
|
||||
}, [migrationsIds, selectedMigrationId]);
|
||||
|
||||
const ruleActionsFactory = useCallback(
|
||||
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
|
||||
// TODO: Add flyout action buttons
|
||||
return null;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
|
||||
ruleActionsFactory,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<NeedAdminForUpdateRulesCallOut />
|
||||
<MissingPrivilegesCallOut />
|
||||
|
||||
<SecuritySolutionPageWrapper>
|
||||
<HeaderPage title={i18n.PAGE_TITLE}>
|
||||
<HeaderButtons
|
||||
migrationsIds={migrationsIds}
|
||||
selectedMigrationId={selectedMigrationId}
|
||||
onMigrationIdChange={onMigrationIdChange}
|
||||
/>
|
||||
</HeaderPage>
|
||||
<EuiSkeletonLoading
|
||||
isLoading={isLoadingMigrationsStats}
|
||||
loadingContent={
|
||||
<>
|
||||
<EuiSkeletonTitle />
|
||||
<EuiSkeletonText />
|
||||
</>
|
||||
}
|
||||
loadedContent={
|
||||
selectedMigrationId ? (
|
||||
<RulesTable migrationId={selectedMigrationId} openRulePreview={openRulePreview} />
|
||||
) : (
|
||||
<NoMigrations />
|
||||
)
|
||||
}
|
||||
/>
|
||||
{rulePreviewFlyout}
|
||||
</SecuritySolutionPageWrapper>
|
||||
|
||||
<SpyRoute pageName={SecurityPageName.siemMigrationsRules} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RulesPage = React.memo(RulesPageComponent);
|
||||
RulesPage.displayName = 'RulesPage';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const PAGE_TITLE = i18n.translate('xpack.securitySolution.siemMigrations.rules.pageTitle', {
|
||||
defaultMessage: 'Translated rules',
|
||||
});
|
|
@ -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';
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 {
|
||||
RuleMigrationTranslationResultEnum,
|
||||
type RuleMigrationTranslationResult,
|
||||
} from '../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => {
|
||||
switch (status) {
|
||||
case RuleMigrationTranslationResultEnum.full:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_FULL_LABEL;
|
||||
|
||||
case RuleMigrationTranslationResultEnum.partial:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_PARTIAL_LABEL;
|
||||
|
||||
case RuleMigrationTranslationResultEnum.untranslatable:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL;
|
||||
|
||||
default:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SIEM_TRANSLATION_RESULT_FULL_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationResult.full',
|
||||
{
|
||||
defaultMessage: 'Fully translated',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIEM_TRANSLATION_RESULT_PARTIAL_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationResult.partially',
|
||||
{
|
||||
defaultMessage: 'Partially translated',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationResult.untranslatable',
|
||||
{
|
||||
defaultMessage: 'Not translated',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationResult.unknown',
|
||||
{
|
||||
defaultMessage: 'Unknown',
|
||||
}
|
||||
);
|
|
@ -83,6 +83,7 @@ import type { EntityAnalytics } from './entity_analytics';
|
|||
import type { Assets } from './assets';
|
||||
import type { Investigations } from './investigations';
|
||||
import type { MachineLearning } from './machine_learning';
|
||||
import type { SiemMigrations } from './siem_migrations';
|
||||
|
||||
import type { Dashboards } from './dashboards';
|
||||
import type { BreadcrumbsNav } from './common/breadcrumbs/types';
|
||||
|
@ -243,6 +244,7 @@ export interface SubPlugins {
|
|||
assets: Assets;
|
||||
investigations: Investigations;
|
||||
machineLearning: MachineLearning;
|
||||
siemMigrations: SiemMigrations;
|
||||
}
|
||||
|
||||
// TODO: find a better way to defined these types
|
||||
|
@ -266,4 +268,5 @@ export interface StartedSubPlugins {
|
|||
assets: ReturnType<Assets['start']>;
|
||||
investigations: ReturnType<Investigations['start']>;
|
||||
machineLearning: ReturnType<MachineLearning['start']>;
|
||||
siemMigrations: ReturnType<SiemMigrations['start']>;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue