[8.x] [Security Solution][SIEM migrations] Onboarding UI improvements (#204320) (#204719)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution][SIEM migrations] Onboarding UI improvements
(#204320)](https://github.com/elastic/kibana/pull/204320)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Sergi
Massaneda","email":"sergi.massaneda@elastic.co"},"sourceCommit":{"committedDate":"2024-12-17T14:42:02Z","message":"[Security
Solution][SIEM migrations] Onboarding UI improvements (#204320)\n\n##
Summary\r\n\r\nPart of:
https://github.com/elastic/security-team/issues/10667\r\n\r\n####
Improvements\r\n\r\n- Implementation of the Onboarding card to create
migrations using the\r\nflyout\r\n- Migration complete summary panel
implemented\r\n- Migration ready panel improved to detect missing
resources\r\n- Migration processing improved\r\n- Migration missing
resources panel implemented\r\n- All migration panels and refactored to
be reusable by translation\r\ntable using the\r\n-
`RuleMigrationDataInputWrapper` implemented to reuse the Flyout
from\r\nthe translation table\r\n- Request poll interval increased from
5 to 10 seconds due to event loop\r\nusage.\r\n\r\n\r\n> [!NOTE] \r\n>
This feature needs `siemMigrationsEnabled` experimental flag
enabled\r\nto work.\r\n\r\n## Screenshots\r\n\r\nLookups
input\r\n\r\n\r\n![Lookups](https://github.com/user-attachments/assets/73f91e10-7252-44d1-ab0d-89880c78a2b3)\r\n\r\nTranslation
\"complete\"
panel\r\n![Translation\r\nsummary](https://github.com/user-attachments/assets/6fbb451d-c7b3-4a23-a2df-083c91948cbd)\r\n\r\nTranslation
\"created\" panel (w/ and w/o missing
macros)\r\n![Ready\r\npanels](https://github.com/user-attachments/assets/f8334af2-ccc1-473c-8548-772a9d656aba)\r\n\r\nTranslation
processing
(preparing)\r\n![preparing\r\npanel](https://github.com/user-attachments/assets/0156caba-c6c9-43c1-881a-8bf631f3a8ab)\r\n\r\nTranslation
processing
(translating)\r\n![translating\r\npanel](https://github.com/user-attachments/assets/db523e4b-4858-482f-bfe9-1e36f715fa20)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"303eee8fee32f5922a87ab7f9cce651d0b2c5735","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Threat
Hunting","Team:
SecuritySolution","backport:version","v8.18.0"],"number":204320,"url":"https://github.com/elastic/kibana/pull/204320","mergeCommit":{"message":"[Security
Solution][SIEM migrations] Onboarding UI improvements (#204320)\n\n##
Summary\r\n\r\nPart of:
https://github.com/elastic/security-team/issues/10667\r\n\r\n####
Improvements\r\n\r\n- Implementation of the Onboarding card to create
migrations using the\r\nflyout\r\n- Migration complete summary panel
implemented\r\n- Migration ready panel improved to detect missing
resources\r\n- Migration processing improved\r\n- Migration missing
resources panel implemented\r\n- All migration panels and refactored to
be reusable by translation\r\ntable using the\r\n-
`RuleMigrationDataInputWrapper` implemented to reuse the Flyout
from\r\nthe translation table\r\n- Request poll interval increased from
5 to 10 seconds due to event loop\r\nusage.\r\n\r\n\r\n> [!NOTE] \r\n>
This feature needs `siemMigrationsEnabled` experimental flag
enabled\r\nto work.\r\n\r\n## Screenshots\r\n\r\nLookups
input\r\n\r\n\r\n![Lookups](https://github.com/user-attachments/assets/73f91e10-7252-44d1-ab0d-89880c78a2b3)\r\n\r\nTranslation
\"complete\"
panel\r\n![Translation\r\nsummary](https://github.com/user-attachments/assets/6fbb451d-c7b3-4a23-a2df-083c91948cbd)\r\n\r\nTranslation
\"created\" panel (w/ and w/o missing
macros)\r\n![Ready\r\npanels](https://github.com/user-attachments/assets/f8334af2-ccc1-473c-8548-772a9d656aba)\r\n\r\nTranslation
processing
(preparing)\r\n![preparing\r\npanel](https://github.com/user-attachments/assets/0156caba-c6c9-43c1-881a-8bf631f3a8ab)\r\n\r\nTranslation
processing
(translating)\r\n![translating\r\npanel](https://github.com/user-attachments/assets/db523e4b-4858-482f-bfe9-1e36f715fa20)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"303eee8fee32f5922a87ab7f9cce651d0b2c5735"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204320","number":204320,"mergeCommit":{"message":"[Security
Solution][SIEM migrations] Onboarding UI improvements (#204320)\n\n##
Summary\r\n\r\nPart of:
https://github.com/elastic/security-team/issues/10667\r\n\r\n####
Improvements\r\n\r\n- Implementation of the Onboarding card to create
migrations using the\r\nflyout\r\n- Migration complete summary panel
implemented\r\n- Migration ready panel improved to detect missing
resources\r\n- Migration processing improved\r\n- Migration missing
resources panel implemented\r\n- All migration panels and refactored to
be reusable by translation\r\ntable using the\r\n-
`RuleMigrationDataInputWrapper` implemented to reuse the Flyout
from\r\nthe translation table\r\n- Request poll interval increased from
5 to 10 seconds due to event loop\r\nusage.\r\n\r\n\r\n> [!NOTE] \r\n>
This feature needs `siemMigrationsEnabled` experimental flag
enabled\r\nto work.\r\n\r\n## Screenshots\r\n\r\nLookups
input\r\n\r\n\r\n![Lookups](https://github.com/user-attachments/assets/73f91e10-7252-44d1-ab0d-89880c78a2b3)\r\n\r\nTranslation
\"complete\"
panel\r\n![Translation\r\nsummary](https://github.com/user-attachments/assets/6fbb451d-c7b3-4a23-a2df-083c91948cbd)\r\n\r\nTranslation
\"created\" panel (w/ and w/o missing
macros)\r\n![Ready\r\npanels](https://github.com/user-attachments/assets/f8334af2-ccc1-473c-8548-772a9d656aba)\r\n\r\nTranslation
processing
(preparing)\r\n![preparing\r\npanel](https://github.com/user-attachments/assets/0156caba-c6c9-43c1-881a-8bf631f3a8ab)\r\n\r\nTranslation
processing
(translating)\r\n![translating\r\npanel](https://github.com/user-attachments/assets/db523e4b-4858-482f-bfe9-1e36f715fa20)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"303eee8fee32f5922a87ab7f9cce651d0b2c5735"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2025-01-08 13:10:57 +01:00 committed by GitHub
parent 45c4faad10
commit 4b931d025f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 1850 additions and 639 deletions

View file

@ -44,7 +44,7 @@ export enum SiemMigrationStatus {
FAILED = 'failed',
}
export enum SiemMigrationRuleTranslationResult {
export enum RuleTranslationResult {
FULL = 'full',
PARTIAL = 'partial',
UNTRANSLATABLE = 'untranslatable',
@ -60,3 +60,5 @@ export const DEFAULT_TRANSLATION_FIELDS = {
to: 'now',
interval: '5m',
} as const;
export const EMPTY_RESOURCE_PLACEHOLDER = '<empty>';

View file

@ -285,21 +285,47 @@ export const RuleMigrationTranslationStats = z.object({
*/
rules: z.object({
/**
* The total number of rules to migrate.
* The total number of rules in the migration.
*/
total: z.number().int(),
/**
* The number of rules that matched Elastic prebuilt rules.
* The number of rules that have been successfully translated.
*/
prebuilt: z.number().int(),
success: z.object({
/**
* The total number of rules that have been successfully translated.
*/
total: z.number().int(),
/**
* The translation results
*/
result: z.object({
/**
* The number of rules that have been fully translated.
*/
full: z.number().int(),
/**
* The number of rules that have been partially translated.
*/
partial: z.number().int(),
/**
* The number of rules that could not be translated.
*/
untranslatable: z.number().int(),
}),
/**
* The number of rules that have been successfully translated and can be installed.
*/
installable: z.number().int(),
/**
* The number of rules that have been successfully translated and matched Elastic prebuilt rules.
*/
prebuilt: z.number().int(),
}),
/**
* The number of rules that did not match Elastic prebuilt rules and will be installed as custom rules.
* The number of rules that have failed translation.
*/
custom: z.number().int(),
/**
* The number of rules that can be installed.
*/
installable: z.number().int(),
failed: z.number().int(),
}),
});

View file

@ -234,23 +234,50 @@ components:
description: The rules migration translation stats.
required:
- total
- prebuilt
- custom
- installable
- success
- failed
properties:
total:
type: integer
description: The total number of rules to migrate.
prebuilt:
description: The total number of rules in the migration.
success:
type: object
description: The number of rules that have been successfully translated.
required:
- total
- result
- installable
- prebuilt
properties:
total:
type: integer
description: The total number of rules that have been successfully translated.
result:
type: object
description: The translation results
required:
- full
- partial
- untranslatable
properties:
full:
type: integer
description: The number of rules that have been fully translated.
partial:
type: integer
description: The number of rules that have been partially translated.
untranslatable:
type: integer
description: The number of rules that could not be translated.
installable:
type: integer
description: The number of rules that have been successfully translated and can be installed.
prebuilt:
type: integer
description: The number of rules that have been successfully translated and matched Elastic prebuilt rules.
failed:
type: integer
description: The number of rules that matched Elastic prebuilt rules.
custom:
type: integer
description: The number of rules that did not match Elastic prebuilt rules and will be installed as custom rules.
installable:
type: integer
description: The number of rules that can be installed.
description: The number of rules that have failed translation.
RuleMigrationTranslationResult:
type: string
description: The rule translation result.

View file

@ -0,0 +1,8 @@
/*
* 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 { PanelText, type PanelTextProps } from './panel_text';

View file

@ -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 React, { type PropsWithChildren } from 'react';
import { css, type CSSInterpolation } from '@emotion/css';
import { EuiText, useEuiTheme, COLOR_MODES_STANDARD, type EuiTextProps } from '@elastic/eui';
export interface PanelTextProps extends PropsWithChildren<EuiTextProps> {
subdued?: true;
semiBold?: true;
}
export const PanelText = React.memo<PanelTextProps>(({ children, subdued, semiBold, ...props }) => {
const { euiTheme, colorMode } = useEuiTheme();
const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark;
let color;
if (subdued && !isDarkMode) {
color = 'subdued';
}
const style: CSSInterpolation = {};
if (semiBold) {
style.fontWeight = euiTheme.font.weight.semiBold;
}
return (
<EuiText {...props} color={color} className={css(style)}>
{children}
</EuiText>
);
});
PanelText.displayName = 'PanelText';

View file

@ -28,7 +28,7 @@ const getCardHash = (cardId: OnboardingCardId | null) => (cardId ? `#${cardId}`
* This hook manages the expanded card id state in the LocalStorage and the hash in the URL.
*/
export const useUrlDetail = () => {
const { spaceId, telemetry } = useOnboardingContext();
const { config, spaceId, telemetry } = useOnboardingContext();
const topicId = useTopicId();
const [storedUrlDetail, setStoredUrlDetail] = useStoredUrlDetails(spaceId);
@ -56,6 +56,14 @@ export const useUrlDetail = () => {
const syncUrlDetails = useCallback(
(pathTopicId: OnboardingTopicId | null, hashCardId: OnboardingCardId | null) => {
if (storedUrlDetail) {
// If the stored topic is not valid, clear it
const [storedTopicId] = storedUrlDetail.split('#');
if (storedTopicId && !config.has(storedTopicId as OnboardingTopicId)) {
setStoredUrlDetail(null);
return;
}
}
const urlDetail = `${pathTopicId || ''}${hashCardId ? `#${hashCardId}` : ''}`;
if (urlDetail && urlDetail !== storedUrlDetail) {
if (hashCardId) {
@ -67,7 +75,7 @@ export const useUrlDetail = () => {
navigateTo({ deepLinkId: SecurityPageName.landing, path: storedUrlDetail });
}
},
[navigateTo, setStoredUrlDetail, storedUrlDetail, telemetry]
[config, navigateTo, setStoredUrlDetail, storedUrlDetail, telemetry]
);
return { setTopicDetail, setCardDetail, syncUrlDetails };

View file

@ -7,6 +7,7 @@
import React, { useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner';
import { useKibana } from '../../../../../../common/lib/kibana/kibana_react';
import { useDefinedLocalStorage } from '../../../../hooks/use_stored_state';
import type { OnboardingCardComponent } from '../../../../../types';
@ -35,9 +36,15 @@ export const AIConnectorCard: OnboardingCardComponent<AIConnectorCardMetadata> =
[setComplete, setStoredConnectorId]
);
const connectors = checkCompleteMetadata?.connectors;
const canExecuteConnectors = checkCompleteMetadata?.canExecuteConnectors;
const canCreateConnectors = checkCompleteMetadata?.canCreateConnectors;
if (!checkCompleteMetadata) {
return (
<OnboardingCardContentPanel>
<CenteredLoadingSpinner />
</OnboardingCardContentPanel>
);
}
const { connectors, canExecuteConnectors, canCreateConnectors } = checkCompleteMetadata;
return (
<OnboardingCardContentPanel>

View file

@ -1,35 +0,0 @@
/*
* 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, { createContext, useContext, useMemo, type PropsWithChildren } from 'react';
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
interface StartMigrationContextValue {
openFlyout: (migrationStats?: RuleMigrationTaskStats) => void;
closeFlyout: () => void;
}
const StartMigrationContext = createContext<StartMigrationContextValue | null>(null);
export const StartMigrationContextProvider: React.FC<
PropsWithChildren<StartMigrationContextValue>
> = React.memo(({ children, openFlyout, closeFlyout }) => {
const value = useMemo<StartMigrationContextValue>(
() => ({ openFlyout, closeFlyout }),
[openFlyout, closeFlyout]
);
return <StartMigrationContext.Provider value={value}>{children}</StartMigrationContext.Provider>;
});
StartMigrationContextProvider.displayName = 'StartMigrationContextProvider';
export const useStartMigrationContext = (): StartMigrationContextValue => {
const context = useContext(StartMigrationContext);
if (context == null) {
throw new Error('useStartMigrationContext must be used within a StartMigrationContextProvider');
}
return context;
};

View file

@ -1,45 +0,0 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiPanel, EuiProgress } from '@elastic/eui';
import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types';
import * as i18n from '../translations';
import { TITLE_CLASS_NAME } from '../start_migration_card.styles';
export interface MigrationProgressPanelProps {
migrationStats: RuleMigrationStats;
}
export const MigrationProgressPanel = React.memo<MigrationProgressPanelProps>(
({ migrationStats }) => {
const progressValue = useMemo(() => {
const finished = migrationStats.rules.completed + migrationStats.rules.failed;
return (finished / migrationStats.rules.total) * 100;
}, [migrationStats.rules]);
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiText size="s" className={TITLE_CLASS_NAME}>
<p>{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<p>{i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiProgress value={progressValue} max={100} color="success" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
);
MigrationProgressPanel.displayName = 'MigrationProgressPanel';

View file

@ -1,68 +0,0 @@
/*
* 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 } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiButton,
EuiButtonEmpty,
EuiPanel,
} from '@elastic/eui';
import { useStartMigration } from '../../../../../../../siem_migrations/rules/service/hooks/use_start_migration';
import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types';
import * as i18n from '../translations';
import { useStartMigrationContext } from '../context';
import { TITLE_CLASS_NAME } from '../start_migration_card.styles';
export interface MigrationReadyPanelProps {
migrationStats: RuleMigrationStats;
}
export const MigrationReadyPanel = React.memo<MigrationReadyPanelProps>(({ migrationStats }) => {
const { openFlyout } = useStartMigrationContext();
const onOpenFlyout = useCallback<React.MouseEventHandler>(() => {
openFlyout(migrationStats);
}, [openFlyout, migrationStats]);
const { startMigration, isLoading } = useStartMigration();
const onStartMigration = useCallback(() => {
startMigration(migrationStats.id);
}, [migrationStats.id, startMigration]);
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
<EuiFlexGroup direction="row" alignItems="center" gutterSize="m">
<EuiFlexItem>
<EuiFlexGroup direction="column" alignItems="flexStart" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText size="s" className={TITLE_CLASS_NAME}>
<p>{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<p>{i18n.START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onStartMigration} isLoading={isLoading}>
{i18n.START_MIGRATION_CARD_TRANSLATE_BUTTON}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton iconType="download" iconSide="right" onClick={onOpenFlyout}>
{i18n.START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
});
MigrationReadyPanel.displayName = 'MigrationReadyPanel';

View file

@ -1,87 +0,0 @@
/*
* 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 moment from 'moment';
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiPanel,
EuiHorizontalRule,
EuiIcon,
} from '@elastic/eui';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar';
import { SecuritySolutionLinkButton } from '../../../../../../../common/components/links';
import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types';
import * as i18n from '../translations';
import { TITLE_CLASS_NAME } from '../start_migration_card.styles';
export interface MigrationResultPanelProps {
migrationStats: RuleMigrationStats;
}
export const MigrationResultPanel = React.memo<MigrationResultPanelProps>(({ migrationStats }) => {
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="none">
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m">
<EuiFlexGroup direction="column" alignItems="flexStart" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiText size="s" className={TITLE_CLASS_NAME}>
<p>{i18n.START_MIGRATION_CARD_RESULT_TITLE(migrationStats.number)}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<p>
{i18n.START_MIGRATION_CARD_RESULT_DESCRIPTION(
moment(migrationStats.created_at).format('MMMM Do YYYY, h:mm:ss a'),
moment(migrationStats.last_updated_at).fromNow()
)}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiHorizontalRule margin="none" />
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m">
<EuiFlexGroup direction="column" alignItems="stretch" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type={AssistantAvatar} size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" className={TITLE_CLASS_NAME}>
<p>{i18n.VIEW_TRANSLATED_RULES_TITLE}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
<EuiFlexGroup direction="column" alignItems="center">
<EuiFlexItem grow={false}>
<p>{'TODO: chart'}</p>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SecuritySolutionLinkButton
deepLinkId={SecurityPageName.siemMigrationsRules}
path={migrationStats.id}
>
{i18n.VIEW_TRANSLATED_RULES_BUTTON}
</SecuritySolutionLinkButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiPanel>
);
});
MigrationResultPanel.displayName = 'MigrationResultPanel';

View file

@ -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 React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants';
import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types';
import { UploadRulesPanel } from './upload_rules_panel';
import { MigrationProgressPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_progress_panel';
import { MigrationResultPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_result_panel';
import { MigrationReadyPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_ready_panel';
import { MissingAIConnectorCallout } from './missing_ai_connector_callout';
export interface RuleMigrationsPanelsProps {
migrationsStats: RuleMigrationStats[];
isConnectorsCardComplete: boolean;
expandConnectorsCard: () => void;
}
export const RuleMigrationsPanels = React.memo<RuleMigrationsPanelsProps>(
({ migrationsStats, isConnectorsCardComplete, expandConnectorsCard }) => {
if (migrationsStats.length === 0) {
return isConnectorsCardComplete ? (
<UploadRulesPanel />
) : (
<MissingAIConnectorCallout onExpandAiConnectorsCard={expandConnectorsCard} />
);
}
return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
{isConnectorsCardComplete ? (
<UploadRulesPanel isUploadMore />
) : (
<MissingAIConnectorCallout onExpandAiConnectorsCard={expandConnectorsCard} />
)}
</EuiFlexItem>
{migrationsStats.map((migrationStats) => (
<EuiFlexItem grow={false} key={migrationStats.id}>
{migrationStats.status === SiemMigrationTaskStatus.READY && (
<MigrationReadyPanel migrationStats={migrationStats} />
)}
{migrationStats.status === SiemMigrationTaskStatus.RUNNING && (
<MigrationProgressPanel migrationStats={migrationStats} />
)}
{migrationStats.status === SiemMigrationTaskStatus.FINISHED && (
<MigrationResultPanel migrationStats={migrationStats} />
)}
</EuiFlexItem>
))}
</EuiFlexGroup>
);
}
);
RuleMigrationsPanels.displayName = 'RuleMigrationsPanels';

View file

@ -5,32 +5,25 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { EuiSpacer, EuiText } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { PanelText } from '../../../../../../common/components/panel_text';
import { RuleMigrationDataInputWrapper } from '../../../../../../siem_migrations/rules/components/data_input_flyout/data_input_wrapper';
import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants';
import { OnboardingCardId } from '../../../../../constants';
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { useLatestStats } from '../../../../../../siem_migrations/rules/service/hooks/use_latest_stats';
import { MigrationDataInputFlyout } from '../../../../../../siem_migrations/rules/components/data_input_flyout';
import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner';
import type { OnboardingCardComponent } from '../../../../../types';
import { OnboardingCardContentPanel } from '../../common/card_content_panel';
import { UploadRulesPanels } from './upload_rules_panels';
import { StartMigrationContextProvider } from './context';
import { RuleMigrationsPanels } from './rule_migrations_panels';
import { useStyles } from './start_migration_card.styles';
import * as i18n from './translations';
import { MissingAIConnectorCallout } from './missing_ai_connector_callout';
export const StartMigrationCard: OnboardingCardComponent = React.memo(
({ setComplete, isCardComplete, setExpandedCardId }) => {
const styles = useStyles();
const { data: migrationsStats, isLoading, refreshStats } = useLatestStats();
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>();
const [flyoutMigrationStats, setFlyoutMigrationStats] = useState<
RuleMigrationTaskStats | undefined
>();
useEffect(() => {
// Set card complete if any migration is finished
if (!isCardComplete(OnboardingCardId.siemMigrationsStart) && migrationsStats) {
@ -40,44 +33,33 @@ export const StartMigrationCard: OnboardingCardComponent = React.memo(
}
}, [isCardComplete, migrationsStats, setComplete]);
const closeFlyout = useCallback(() => {
setIsFlyoutOpen(false);
setFlyoutMigrationStats(undefined);
refreshStats();
}, [refreshStats]);
const isConnectorsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.siemMigrationsAiConnectors),
[isCardComplete]
);
const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => {
setFlyoutMigrationStats(migrationStats);
setIsFlyoutOpen(true);
}, []);
if (!isCardComplete(OnboardingCardId.siemMigrationsAiConnectors)) {
return (
<MissingAIConnectorCallout
onExpandAiConnectorsCard={() =>
setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors)
}
/>
);
}
const expandConnectorsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors);
}, [setExpandedCardId]);
return (
<StartMigrationContextProvider openFlyout={openFlyout} closeFlyout={closeFlyout}>
<RuleMigrationDataInputWrapper onFlyoutClosed={refreshStats}>
<OnboardingCardContentPanel paddingSize="none" className={styles}>
{isLoading ? (
<CenteredLoadingSpinner />
) : (
<UploadRulesPanels migrationsStats={migrationsStats} />
<RuleMigrationsPanels
migrationsStats={migrationsStats}
isConnectorsCardComplete={isConnectorsCardComplete}
expandConnectorsCard={expandConnectorsCard}
/>
)}
<EuiSpacer size="m" />
<EuiText size="xs" color="subdued">
<PanelText size="xs" subdued>
<p>{i18n.START_MIGRATION_CARD_FOOTER_NOTE}</p>
</EuiText>
</PanelText>
</OnboardingCardContentPanel>
{isFlyoutOpen && (
<MigrationDataInputFlyout onClose={closeFlyout} migrationStats={flyoutMigrationStats} />
)}
</StartMigrationContextProvider>
</RuleMigrationDataInputWrapper>
);
}
);

View file

@ -55,64 +55,3 @@ export const START_MIGRATION_CARD_UPLOAD_MORE_BUTTON = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.uploadMore.button',
{ defaultMessage: 'Upload more rules' }
);
export const START_MIGRATION_CARD_UPLOAD_READ_MORE = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.upload.readMore',
{ defaultMessage: 'Read more about our AI powered translations and other features.' }
);
export const START_MIGRATION_CARD_UPLOAD_READ_DOCS = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.upload.readAiDocsLink',
{ defaultMessage: 'Read AI docs' }
);
export const START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.ready.description',
{
defaultMessage:
'Migration is created and ready but the translation has not started yet. You can either upload macros & lookups or start the translation process',
}
);
export const START_MIGRATION_CARD_TRANSLATE_BUTTON = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.translate.button',
{ defaultMessage: 'Start translation' }
);
export const START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.uploadMacros.button',
{ defaultMessage: 'Upload macros' }
);
export const START_MIGRATION_CARD_MIGRATION_TITLE = (number: number) =>
i18n.translate('xpack.securitySolution.onboarding.startMigration.migrationTitle', {
defaultMessage: 'SIEM rules migration #{number}',
values: { number },
});
export const START_MIGRATION_CARD_PROGRESS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.progress.description',
{
defaultMessage: `This may take a few minutes & the task will work in the background. Just stay logged in and we'll notify you when done.`,
}
);
export const START_MIGRATION_CARD_RESULT_TITLE = (number: number) =>
i18n.translate('xpack.securitySolution.onboarding.startMigration.result.title', {
defaultMessage: 'SIEM rules migration #{number} complete',
values: { number },
});
export const START_MIGRATION_CARD_RESULT_DESCRIPTION = (createdAt: string, finishedAt: string) =>
i18n.translate('xpack.securitySolution.onboarding.startMigration.result.description', {
defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.',
values: { createdAt, finishedAt },
});
export const VIEW_TRANSLATED_RULES_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.result.translatedRules.title',
{ defaultMessage: 'Translation Summary' }
);
export const VIEW_TRANSLATED_RULES_BUTTON = i18n.translate(
'xpack.securitySolution.onboarding.startMigration.result.translatedRules.button',
{ defaultMessage: 'View translated rules' }
);

View file

@ -15,10 +15,11 @@ import {
EuiButtonEmpty,
EuiPanel,
} from '@elastic/eui';
import { SiemMigrationsIcon } from '../../../../../../../siem_migrations/common/icon';
import * as i18n from '../translations';
import { useStartMigrationContext } from '../context';
import { TITLE_CLASS_NAME } from '../start_migration_card.styles';
import { RuleMigrationsReadMore } from '../../../../../../siem_migrations/rules/components/migration_status_panels/read_more';
import { SiemMigrationsIcon } from '../../../../../../siem_migrations/common/icon';
import * as i18n from './translations';
import { TITLE_CLASS_NAME } from './start_migration_card.styles';
import { useRuleMigrationDataInputContext } from '../../../../../../siem_migrations/rules/components/data_input_flyout/context';
import { useStyles } from './upload_rules_panel.styles';
export interface UploadRulesPanelProps {
@ -26,7 +27,7 @@ export interface UploadRulesPanelProps {
}
export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(({ isUploadMore = false }) => {
const styles = useStyles(isUploadMore);
const { openFlyout } = useStartMigrationContext();
const { openFlyout } = useRuleMigrationDataInputContext();
const onOpenFlyout = useCallback<React.MouseEventHandler>(() => {
openFlyout();
}, [openFlyout]);
@ -55,9 +56,7 @@ export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(({ isUploadMor
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs">
<p>{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}</p>
</EuiText>
<RuleMigrationsReadMore />
</EuiFlexItem>
</EuiFlexGroup>
)}

View file

@ -1,46 +0,0 @@
/*
* 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, EuiFlexItem } from '@elastic/eui';
import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants';
import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types';
import { UploadRulesPanel } from './panels/upload_rules_panel';
import { MigrationProgressPanel } from './panels/migration_progress_panel';
import { MigrationResultPanel } from './panels/migration_result_panel';
import { MigrationReadyPanel } from './panels/migration_ready_panel';
export interface UploadRulesPanelsProps {
migrationsStats: RuleMigrationStats[];
}
export const UploadRulesPanels = React.memo<UploadRulesPanelsProps>(({ migrationsStats }) => {
if (migrationsStats.length === 0) {
return <UploadRulesPanel />;
}
return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<UploadRulesPanel isUploadMore />
</EuiFlexItem>
{migrationsStats.map((migrationStats) => (
<EuiFlexItem grow={false} key={migrationStats.id}>
{migrationStats.status === SiemMigrationTaskStatus.READY && (
<MigrationReadyPanel migrationStats={migrationStats} />
)}
{migrationStats.status === SiemMigrationTaskStatus.RUNNING && (
<MigrationProgressPanel migrationStats={migrationStats} />
)}
{migrationStats.status === SiemMigrationTaskStatus.FINISHED && (
<MigrationResultPanel migrationStats={migrationStats} />
)}
</EuiFlexItem>
))}
</EuiFlexGroup>
);
});
UploadRulesPanels.displayName = 'UploadRulesPanels';

View file

@ -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 React, { createContext, useContext, useMemo, type PropsWithChildren } from 'react';
import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
interface RuleMigrationDataInputContextValue {
openFlyout: (migrationStats?: RuleMigrationTaskStats) => void;
closeFlyout: () => void;
}
const RuleMigrationDataInputContext = createContext<RuleMigrationDataInputContextValue | null>(
null
);
export const RuleMigrationDataInputContextProvider: React.FC<
PropsWithChildren<RuleMigrationDataInputContextValue>
> = React.memo(({ children, openFlyout, closeFlyout }) => {
const value = useMemo<RuleMigrationDataInputContextValue>(
() => ({ openFlyout, closeFlyout }),
[openFlyout, closeFlyout]
);
return (
<RuleMigrationDataInputContext.Provider value={value}>
{children}
</RuleMigrationDataInputContext.Provider>
);
});
RuleMigrationDataInputContextProvider.displayName = 'RuleMigrationDataInputContextProvider';
export const useRuleMigrationDataInputContext = (): RuleMigrationDataInputContextValue => {
const context = useContext(RuleMigrationDataInputContext);
if (context == null) {
throw new Error(
'useRuleMigrationDataInputContext must be used within a RuleMigrationDataInputContextProvider'
);
}
return context;
};

View file

@ -15,6 +15,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type {
@ -23,8 +24,9 @@ import type {
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { RulesDataInput } from './steps/rules/rules_data_input';
import { useStartMigration } from '../../service/hooks/use_start_migration';
import { DataInputStep } from './types';
import { DataInputStep } from './steps/constants';
import { MacrosDataInput } from './steps/macros/macros_data_input';
import { LookupsDataInput } from './steps/lookups/lookups_data_input';
interface MissingResourcesIndexed {
macros: string[];
@ -84,8 +86,8 @@ export const MigrationDataInputFlyout = React.memo<MigrationDataInputFlyoutProps
[]
);
const onMacrosCreated = useCallback(() => {
setDataInputStep(DataInputStep.Lookups);
const onAllLookupsCreated = useCallback(() => {
setDataInputStep(DataInputStep.End);
}, []);
return (
@ -121,21 +123,28 @@ export const MigrationDataInputFlyout = React.memo<MigrationDataInputFlyoutProps
dataInputStep={dataInputStep}
missingMacros={missingResourcesIndexed?.macros}
migrationStats={migrationStats}
onMacrosCreated={onMacrosCreated}
onMissingResourcesFetched={onMissingResourcesFetched}
/>
</EuiFlexItem>
<EuiFlexItem>
<LookupsDataInput
dataInputStep={dataInputStep}
missingLookups={missingResourcesIndexed?.lookups}
migrationStats={migrationStats}
onAllLookupsCreated={onAllLookupsCreated}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onClose}>
<EuiButtonEmpty onClick={onClose}>
<FormattedMessage
id="xpack.securitySolution.siemMigrations.rules.dataInputFlyout.closeButton"
defaultMessage="Close"
/>
</EuiButton>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton

View file

@ -0,0 +1,45 @@
/*
* 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 { PropsWithChildren } from 'react';
import React, { useCallback, useState } from 'react';
import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { RuleMigrationDataInputContextProvider } from './context';
import { MigrationDataInputFlyout } from './data_input_flyout';
interface RuleMigrationDataInputWrapperProps {
onFlyoutClosed: () => void;
}
export const RuleMigrationDataInputWrapper = React.memo<
PropsWithChildren<RuleMigrationDataInputWrapperProps>
>(({ children, onFlyoutClosed }) => {
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>();
const [flyoutMigrationStats, setFlyoutMigrationStats] = useState<
RuleMigrationTaskStats | undefined
>();
const closeFlyout = useCallback(() => {
setIsFlyoutOpen(false);
setFlyoutMigrationStats(undefined);
onFlyoutClosed?.();
}, [onFlyoutClosed]);
const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => {
setFlyoutMigrationStats(migrationStats);
setIsFlyoutOpen(true);
}, []);
return (
<RuleMigrationDataInputContextProvider openFlyout={openFlyout} closeFlyout={closeFlyout}>
{children}
{isFlyoutOpen && (
<MigrationDataInputFlyout onClose={closeFlyout} migrationStats={flyoutMigrationStats} />
)}
</RuleMigrationDataInputContextProvider>
);
});
RuleMigrationDataInputWrapper.displayName = 'RuleMigrationDataInputWrapper';

View file

@ -5,9 +5,8 @@
* 2.0.
*/
import { EuiPanel } from '@elastic/eui';
import { EuiPanel, EuiSteps, type EuiStepProps } from '@elastic/eui';
import { css } from '@emotion/css';
import type { PropsWithChildren } from 'react';
import React from 'react';
const style = css`
@ -16,11 +15,11 @@ const style = css`
}
`;
export const SubStepsWrapper = React.memo<PropsWithChildren<{}>>(({ children }) => {
export const SubSteps = React.memo<{ steps: EuiStepProps[] }>(({ steps }) => {
return (
<EuiPanel hasShadow={false} paddingSize="xs" className={style}>
{children}
<EuiSteps titleSize="xxs" steps={steps} />
</EuiPanel>
);
});
SubStepsWrapper.displayName = 'SubStepsWrapper';
SubSteps.displayName = 'SubSteps';

View file

@ -26,7 +26,7 @@ export const useParseFileInput = (onFileParsed: OnFileParsed) => {
setError(undefined);
const rulesFile = files[0];
const file = files[0];
const reader = new FileReader();
reader.onloadstart = () => setIsParsing(true);
@ -68,7 +68,7 @@ export const useParseFileInput = (onFileParsed: OnFileParsed) => {
reader.onerror = handleReaderError;
reader.onabort = handleReaderError;
reader.readAsText(rulesFile);
reader.readAsText(file);
},
[onFileParsed]
);

View file

@ -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.
*/
export enum DataInputStep {
Rules = 1,
Macros = 2,
Lookups = 3,
End = 10,
}

View file

@ -0,0 +1,143 @@
/*
* 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 { EuiStepProps } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiStepNumber,
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../common/siem_migrations/constants';
import type {
RuleMigrationResourceData,
RuleMigrationTaskStats,
} from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { OnResourcesCreated } from '../../types';
import { getStatus } from '../common/get_status';
import * as i18n from './translations';
import { DataInputStep } from '../constants';
import { SubSteps } from '../common/sub_step';
import { useMissingLookupsListStep } from './sub_steps/missing_lookups_list';
import { useLookupsFileUploadStep } from './sub_steps/lookups_file_upload';
export type UploadedLookups = Record<string, string>;
export type AddUploadedLookups = (lookups: RuleMigrationResourceData[]) => void;
interface LookupsDataInputSubStepsProps {
migrationStats: RuleMigrationTaskStats;
missingLookups: string[];
onAllLookupsCreated: OnResourcesCreated;
}
interface LookupsDataInputProps
extends Omit<LookupsDataInputSubStepsProps, 'migrationStats' | 'missingLookups'> {
dataInputStep: DataInputStep;
migrationStats?: RuleMigrationTaskStats;
missingLookups?: string[];
}
export const LookupsDataInput = React.memo<LookupsDataInputProps>(
({ dataInputStep, migrationStats, missingLookups, onAllLookupsCreated }) => {
const dataInputStatus = useMemo(
() => getStatus(DataInputStep.Lookups, dataInputStep),
[dataInputStep]
);
return (
<EuiPanel hasShadow={false} hasBorder>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup direction="row" justifyContent="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiStepNumber
titleSize="xs"
number={DataInputStep.Lookups}
status={dataInputStatus}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<b>{i18n.LOOKUPS_DATA_INPUT_TITLE}</b>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{dataInputStatus === 'current' && migrationStats && missingLookups && (
<>
<EuiFlexItem>
<EuiText size="s" color="subdued">
{i18n.LOOKUPS_DATA_INPUT_DESCRIPTION}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<LookupsDataInputSubSteps
migrationStats={migrationStats}
missingLookups={missingLookups}
onAllLookupsCreated={onAllLookupsCreated}
/>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
</EuiPanel>
);
}
);
LookupsDataInput.displayName = 'LookupsDataInput';
const END = 10 as const;
type SubStep = 1 | 2 | typeof END;
export const LookupsDataInputSubSteps = React.memo<LookupsDataInputSubStepsProps>(
({ migrationStats, missingLookups, onAllLookupsCreated }) => {
const [subStep, setSubStep] = useState<SubStep>(1);
const [uploadedLookups, setUploadedLookups] = useState<UploadedLookups>({});
const addUploadedLookups = useCallback<AddUploadedLookups>((lookups) => {
setUploadedLookups((prevUploadedLookups) => ({
...prevUploadedLookups,
...Object.fromEntries(
lookups.map((lookup) => [lookup.name, lookup.content ?? EMPTY_RESOURCE_PLACEHOLDER])
),
}));
}, []);
useEffect(() => {
if (missingLookups.every((lookupName) => uploadedLookups[lookupName])) {
setSubStep(END);
onAllLookupsCreated();
}
}, [uploadedLookups, missingLookups, onAllLookupsCreated]);
// Copy query step
const onCopied = useCallback(() => {
setSubStep(2);
}, []);
const copyStep = useMissingLookupsListStep({
status: getStatus(1, subStep),
migrationStats,
missingLookups,
uploadedLookups,
addUploadedLookups,
onCopied,
});
// Upload macros step
const uploadStep = useLookupsFileUploadStep({
status: getStatus(2, subStep),
migrationStats,
missingLookups,
addUploadedLookups,
});
const steps = useMemo<EuiStepProps[]>(() => [copyStep, uploadStep], [copyStep, uploadStep]);
return <SubSteps steps={steps} />;
}
);
LookupsDataInputSubSteps.displayName = 'LookupsDataInputActive';

View file

@ -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 React, { useCallback, useMemo } from 'react';
import type { EuiStepProps, EuiStepStatus } from '@elastic/eui';
import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources';
import type {
RuleMigrationResourceData,
RuleMigrationTaskStats,
} from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { AddUploadedLookups } from '../../lookups_data_input';
import * as i18n from './translations';
import { LookupsFileUpload } from './lookups_file_upload';
export interface RulesFileUploadStepProps {
status: EuiStepStatus;
migrationStats: RuleMigrationTaskStats;
missingLookups: string[];
addUploadedLookups: AddUploadedLookups;
}
export const useLookupsFileUploadStep = ({
status,
migrationStats,
addUploadedLookups,
}: RulesFileUploadStepProps): EuiStepProps => {
const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups);
const upsertMigrationResources = useCallback(
(lookupsFromFile: RuleMigrationResourceData[]) => {
if (lookupsFromFile.length === 0) {
return; // No lookups provided
}
upsertResources(migrationStats.id, lookupsFromFile);
},
[upsertResources, migrationStats]
);
const uploadStepStatus = useMemo(() => {
if (isLoading) {
return 'loading';
}
if (error) {
return 'danger';
}
return status;
}, [isLoading, error, status]);
return {
title: i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_TITLE,
status: uploadStepStatus,
children: (
<LookupsFileUpload
createResources={upsertMigrationResources}
isLoading={isLoading}
apiError={error?.message}
/>
),
};
};

View file

@ -0,0 +1,163 @@
/*
* 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, useMemo, useRef, useState } from 'react';
import {
EuiButton,
EuiFilePicker,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiText,
} from '@elastic/eui';
import type {
EuiFilePickerClass,
EuiFilePickerProps,
} from '@elastic/eui/src/components/form/file_picker/file_picker';
import type { RuleMigrationResourceData } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { FILE_UPLOAD_ERROR } from '../../../../translations';
import * as i18n from './translations';
export interface LookupsFileUploadProps {
createResources: (resources: RuleMigrationResourceData[]) => void;
apiError?: string;
isLoading?: boolean;
}
export const LookupsFileUpload = React.memo<LookupsFileUploadProps>(
({ createResources, apiError, isLoading }) => {
const [lookupResources, setLookupResources] = useState<RuleMigrationResourceData[]>([]);
const filePickerRef = useRef<EuiFilePickerClass>(null);
const createLookups = useCallback(() => {
filePickerRef.current?.removeFiles();
createResources(lookupResources);
}, [createResources, lookupResources]);
const [isParsing, setIsParsing] = useState<boolean>(false);
const [fileErrors, setErrors] = useState<string[]>([]);
const addError = useCallback((error: string) => {
setErrors((current) => [...current, error]);
}, []);
const parseFile = useCallback(
async (files: FileList | null) => {
setErrors([]);
setLookupResources([]);
if (!files?.length) {
return;
}
const lookups = await Promise.all(
Array.from(files).map((file) => {
return new Promise<RuleMigrationResourceData>((resolve) => {
const reader = new FileReader();
reader.onloadstart = () => setIsParsing(true);
reader.onloadend = () => setIsParsing(false);
reader.onload = function (e) {
// We can safely cast to string since we call `readAsText` to load the file.
const content = e.target?.result as string | undefined;
if (content == null) {
addError(FILE_UPLOAD_ERROR.CAN_NOT_READ);
return;
}
if (content === '' && e.loaded > 100000) {
// V8-based browsers can't handle large files and return an empty string
// instead of an error; see https://stackoverflow.com/a/61316641
addError(FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE);
return;
}
const name = file.name.replace(/\.[^/.]+$/, '').trim();
resolve({ type: 'list', name, content });
};
const handleReaderError = function () {
const message = reader.error?.message;
if (message) {
addError(FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message));
} else {
addError(FILE_UPLOAD_ERROR.CAN_NOT_READ);
}
};
reader.onerror = handleReaderError;
reader.onabort = handleReaderError;
reader.readAsText(file);
});
})
).catch((e) => {
addError(e.message);
return [];
});
// Set the loaded lookups to the state
setLookupResources((current) => [...current, ...lookups]);
},
[addError]
);
const errors = useMemo(() => {
if (apiError) {
return [apiError];
}
return fileErrors;
}, [apiError, fileErrors]);
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFormRow
helpText={errors.map((error) => (
<EuiText color="danger" size="xs">
{error}
</EuiText>
))}
isInvalid={errors.length > 0}
fullWidth
>
<EuiFilePicker
id="lookupsFilePicker"
ref={filePickerRef as React.Ref<Omit<EuiFilePickerProps, 'stylesMemoizer'>>}
fullWidth
initialPromptText={
<>
<EuiText size="s" textAlign="center">
{i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_PROMPT}
</EuiText>
</>
}
accept="application/text"
onChange={parseFile}
multiple
display="large"
aria-label="Upload lookups files"
isLoading={isParsing || isLoading}
disabled={isParsing || isLoading}
data-test-subj="lookupsFilePicker"
data-loading={isParsing}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiButton onClick={createLookups} isLoading={isLoading} color="success">
{i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_BUTTON}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
LookupsFileUpload.displayName = 'LookupsFileUpload';

View file

@ -0,0 +1,34 @@
/*
* 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 LOOKUPS_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.title',
{ defaultMessage: 'Update your lookups export' }
);
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.prompt',
{ defaultMessage: 'Select or drag and drop the exported lookup files' }
);
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_NOT_UPLOADED_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.notUploadedTitle',
{ defaultMessage: 'Lookups not uploaded' }
);
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_NOT_UPLOADED = (lookupsNames: string) =>
i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.notUploaded',
{
defaultMessage: 'The following files did not match any missing lookup: {lookupsNames}',
values: { lookupsNames },
}
);
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_BUTTON = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.button',
{ defaultMessage: 'Upload' }
);

View file

@ -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 React, { useCallback, useMemo } from 'react';
import type { EuiStepProps, EuiStepStatus } from '@elastic/eui';
import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../../../common/siem_migrations/constants';
import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources';
import type { RuleMigrationTaskStats } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { UploadedLookups, AddUploadedLookups } from '../../lookups_data_input';
import * as i18n from './translations';
import { MissingLookupsList } from './missing_lookups_list';
export interface MissingLookupsListStepProps {
status: EuiStepStatus;
migrationStats: RuleMigrationTaskStats;
missingLookups: string[];
uploadedLookups: UploadedLookups;
addUploadedLookups: AddUploadedLookups;
onCopied: () => void;
}
export const useMissingLookupsListStep = ({
status,
migrationStats,
missingLookups,
uploadedLookups,
addUploadedLookups,
onCopied,
}: MissingLookupsListStepProps): EuiStepProps => {
const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups);
const clearLookup = useCallback(
(lookupName: string) => {
upsertResources(migrationStats.id, [
{ type: 'list', name: lookupName, content: EMPTY_RESOURCE_PLACEHOLDER },
]);
},
[upsertResources, migrationStats]
);
const listStepStatus = useMemo(() => {
if (isLoading) {
return 'loading';
}
if (error) {
return 'danger';
}
return status;
}, [isLoading, error, status]);
return {
title: i18n.LOOKUPS_DATA_INPUT_COPY_TITLE,
status: listStepStatus,
children: (
<MissingLookupsList
onCopied={onCopied}
missingLookups={missingLookups}
uploadedLookups={uploadedLookups}
clearLookup={clearLookup}
/>
),
};
};

View file

@ -0,0 +1,161 @@
/*
* 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, useMemo, useState } from 'react';
import { css } from '@emotion/css';
import {
EuiButtonIcon,
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiSpacer,
EuiText,
EuiToolTip,
useEuiTheme,
} from '@elastic/eui';
import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../../../common/siem_migrations/constants';
import type { UploadedLookups } from '../../lookups_data_input';
import * as i18n from './translations';
const scrollPanelCss = css`
max-height: 200px;
overflow-y: auto;
`;
interface MissingLookupsListProps {
missingLookups: string[];
uploadedLookups: UploadedLookups;
clearLookup: (lookupsName: string) => void;
onCopied: () => void;
}
export const MissingLookupsList = React.memo<MissingLookupsListProps>(
({ missingLookups, uploadedLookups, clearLookup, onCopied }) => {
const { euiTheme } = useEuiTheme();
return (
<>
<EuiPanel hasShadow={false} hasBorder className={scrollPanelCss}>
<EuiFlexGroup direction="column" gutterSize="s">
{missingLookups.map((lookupName) => {
const isMarkedAsEmpty = uploadedLookups[lookupName] === EMPTY_RESOURCE_PLACEHOLDER;
return (
<EuiFlexItem key={lookupName}>
<EuiFlexGroup
direction="row"
gutterSize="s"
alignItems="center"
justifyContent="flexStart"
>
<EuiFlexItem grow={false}>
{uploadedLookups[lookupName] ? (
<EuiIcon type="checkInCircleFilled" color={euiTheme.colors.success} />
) : (
<EuiIcon type="dot" />
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText
size="s"
style={isMarkedAsEmpty ? { textDecoration: 'line-through' } : {}}
>
{lookupName}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy textToCopy={lookupName}>
{(copy) => (
<CopyLookupNameButton
lookupName={lookupName}
onCopied={onCopied}
copy={copy}
/>
)}
</EuiCopy>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ClearLookupButton
lookupName={lookupName}
clearLookup={clearLookup}
isDisabled={isMarkedAsEmpty}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiPanel>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
{i18n.MISSING_LOOKUPS_DESCRIPTION}
</EuiText>
</>
);
}
);
MissingLookupsList.displayName = 'MissingLookupsList';
interface CopyLookupNameButtonProps {
lookupName: string;
onCopied: () => void;
copy: () => void;
}
const CopyLookupNameButton = React.memo<CopyLookupNameButtonProps>(
({ lookupName, onCopied, copy }) => {
const onClick = useCallback(() => {
copy();
onCopied();
}, [copy, onCopied]);
return (
<EuiToolTip content={i18n.COPY_LOOKUP_NAME_TOOLTIP}>
<EuiButtonIcon
onClick={onClick}
iconType="copyClipboard"
color="text"
aria-label={`${i18n.COPY_LOOKUP_NAME_TOOLTIP} ${lookupName}`}
data-test-subj="lookupNameCopy"
/>
</EuiToolTip>
);
}
);
CopyLookupNameButton.displayName = 'CopyLookupNameButton';
interface ClearLookupButtonProps {
lookupName: string;
clearLookup: (lookupName: string) => void;
isDisabled: boolean;
}
const ClearLookupButton = React.memo<ClearLookupButtonProps>(
({ lookupName, clearLookup, isDisabled: isDisabledDefault }) => {
const [isDisabled, setIsDisabled] = useState(isDisabledDefault);
const onClick = useCallback(() => {
setIsDisabled(true);
clearLookup(lookupName);
}, [clearLookup, lookupName]);
const button = useMemo(
() => (
<EuiButtonIcon
onClick={onClick}
iconType="cross"
color="text"
aria-label={i18n.CLEAR_EMPTY_LOOKUP_TOOLTIP}
data-test-subj="lookupNameClear"
isDisabled={isDisabled}
/>
),
[onClick, isDisabled]
);
if (isDisabled) {
return button;
}
return <EuiToolTip content={i18n.CLEAR_EMPTY_LOOKUP_TOOLTIP}>{button}</EuiToolTip>;
}
);
ClearLookupButton.displayName = 'ClearLookupButton';

View file

@ -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 { i18n } from '@kbn/i18n';
export const LOOKUPS_DATA_INPUT_COPY_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.title',
{ defaultMessage: 'Lookups found in your rules' }
);
export const MISSING_LOOKUPS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.description',
{
defaultMessage:
'For your lookups, go to your admin Splunk account and the Search and Reporting app Lookups page. Download the following lookups individually and upload below.',
}
);
export const COPY_LOOKUP_NAME_TOOLTIP = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.copyLookupNameTooltip',
{ defaultMessage: 'Copy lookup name' }
);
export const CLEAR_EMPTY_LOOKUP_TOOLTIP = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.clearEmptyLookupTooltip',
{ defaultMessage: 'Mark the lookup as empty' }
);

View file

@ -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 { i18n } from '@kbn/i18n';
export const LOOKUPS_DATA_INPUT_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.title',
{ defaultMessage: 'Upload identified lookups' }
);
export const LOOKUPS_DATA_INPUT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.description',
{
defaultMessage: `We've also found lookups within your rules. To fully translate those rules containing these lookups, follow the step-by-step guide to export and upload them all.`,
}
);

View file

@ -6,30 +6,21 @@
*/
import type { EuiStepProps } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiStepNumber,
EuiSteps,
EuiTitle,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStepNumber, EuiTitle } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { SubStepsWrapper } from '../common/sub_step_wrapper';
import type { OnResourcesCreated, OnMissingResourcesFetched, DataInputStep } from '../../types';
import type { OnResourcesCreated, OnMissingResourcesFetched } from '../../types';
import { getStatus } from '../common/get_status';
import * as i18n from './translations';
import { DataInputStep } from '../constants';
import { SubSteps } from '../common/sub_step';
import { useCopyExportQueryStep } from './sub_steps/copy_export_query';
import { useMacrosFileUploadStep } from './sub_steps/macros_file_upload';
import * as i18n from './translations';
import { useCheckResourcesStep } from './sub_steps/check_resources';
const DataInputStepNumber: DataInputStep = 2;
interface MacrosDataInputSubStepsProps {
migrationStats: RuleMigrationTaskStats;
missingMacros: string[];
onMacrosCreated: OnResourcesCreated;
onMissingResourcesFetched: OnMissingResourcesFetched;
}
interface MacrosDataInputProps
@ -39,15 +30,9 @@ interface MacrosDataInputProps
missingMacros?: string[];
}
export const MacrosDataInput = React.memo<MacrosDataInputProps>(
({
dataInputStep,
migrationStats,
missingMacros,
onMacrosCreated,
onMissingResourcesFetched,
}) => {
({ dataInputStep, migrationStats, missingMacros, onMissingResourcesFetched }) => {
const dataInputStatus = useMemo(
() => getStatus(DataInputStepNumber, dataInputStep),
() => getStatus(DataInputStep.Macros, dataInputStep),
[dataInputStep]
);
@ -59,7 +44,7 @@ export const MacrosDataInput = React.memo<MacrosDataInputProps>(
<EuiFlexItem grow={false}>
<EuiStepNumber
titleSize="xs"
number={DataInputStepNumber}
number={DataInputStep.Macros}
status={dataInputStatus}
/>
</EuiFlexItem>
@ -75,7 +60,6 @@ export const MacrosDataInput = React.memo<MacrosDataInputProps>(
<MacrosDataInputSubSteps
migrationStats={migrationStats}
missingMacros={missingMacros}
onMacrosCreated={onMacrosCreated}
onMissingResourcesFetched={onMissingResourcesFetched}
/>
</EuiFlexItem>
@ -90,7 +74,7 @@ MacrosDataInput.displayName = 'MacrosDataInput';
const END = 10 as const;
type SubStep = 1 | 2 | 3 | typeof END;
export const MacrosDataInputSubSteps = React.memo<MacrosDataInputSubStepsProps>(
({ migrationStats, missingMacros, onMacrosCreated, onMissingResourcesFetched }) => {
({ migrationStats, missingMacros, onMissingResourcesFetched }) => {
const [subStep, setSubStep] = useState<SubStep>(missingMacros.length ? 1 : 3);
// Copy query step
@ -101,9 +85,8 @@ export const MacrosDataInputSubSteps = React.memo<MacrosDataInputSubStepsProps>(
// Upload macros step
const onMacrosCreatedStep = useCallback<OnResourcesCreated>(() => {
onMacrosCreated();
setSubStep(3);
}, [onMacrosCreated]);
}, []);
const uploadStep = useMacrosFileUploadStep({
status: getStatus(2, subStep),
migrationStats,
@ -130,11 +113,7 @@ export const MacrosDataInputSubSteps = React.memo<MacrosDataInputSubStepsProps>(
[copyStep, uploadStep, resourcesStep]
);
return (
<SubStepsWrapper>
<EuiSteps titleSize="xxs" steps={steps} />
</SubStepsWrapper>
);
return <SubSteps steps={steps} />;
}
);
MacrosDataInputSubSteps.displayName = 'MacrosDataInputActive';

View file

@ -42,7 +42,7 @@ export const CopyExportQuery = React.memo<CopyExportQueryProps>(({ onCopied }) =
id="xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.description"
defaultMessage="From you admin Splunk account, go to the {section} app and run the above query. Export your results as {format}."
values={{
section: <b>{i18n.RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION}</b>,
section: <b>{i18n.MACROS_DATA_INPUT_COPY_DESCRIPTION_SECTION}</b>,
format: <b>{'JSON'}</b>,
}}
/>

View file

@ -19,7 +19,7 @@ export const useCopyExportQueryStep = ({
onCopied,
}: CopyExportQueryStepProps): EuiStepProps => {
return {
title: i18n.RULES_DATA_INPUT_COPY_TITLE,
title: i18n.MACROS_DATA_INPUT_COPY_TITLE,
status,
children: <CopyExportQuery onCopied={onCopied} />,
};

View file

@ -7,12 +7,12 @@
import { i18n } from '@kbn/i18n';
export const RULES_DATA_INPUT_COPY_TITLE = i18n.translate(
export const MACROS_DATA_INPUT_COPY_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.copyExportQuery.title',
{ defaultMessage: 'Copy macros query' }
);
export const RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate(
export const MACROS_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.copyExportQuery.description.section',
{ defaultMessage: 'Search and Reporting' }
);

View file

@ -81,7 +81,7 @@ export const useMacrosFileUploadStep = ({
}, [isLoading, error, status]);
return {
title: i18n.RULES_DATA_INPUT_FILE_UPLOAD_TITLE,
title: i18n.MACROS_DATA_INPUT_FILE_UPLOAD_TITLE,
status: uploadStepStatus,
children: (
<MacrosFileUpload

View file

@ -25,8 +25,8 @@ export const MacrosFileUpload = React.memo<MacrosFileUploadProps>(
({ createResources, apiError, isLoading }) => {
const onFileParsed = useCallback(
(content: Array<SplunkRow<SplunkMacroResult>>) => {
const rules = content.map(formatMacroRow);
createResources(rules);
const macros = content.map(formatMacroRow);
createResources(macros);
},
[createResources]
);
@ -56,14 +56,14 @@ export const MacrosFileUpload = React.memo<MacrosFileUploadProps>(
initialPromptText={
<>
<EuiText size="s" textAlign="center">
{i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT}
{i18n.MACROS_DATA_INPUT_FILE_UPLOAD_PROMPT}
</EuiText>
</>
}
accept="application/json, application/x-ndjson"
onChange={parseFile}
display="large"
aria-label="Upload logs sample file"
aria-label="Upload macros file"
isLoading={isParsing || isLoading}
disabled={isParsing || isLoading}
data-test-subj="macrosFilePicker"

View file

@ -7,20 +7,11 @@
import { i18n } from '@kbn/i18n';
export const RULES_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate(
export const MACROS_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.title',
{ defaultMessage: 'Update your macros export' }
);
export const RULES_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate(
export const MACROS_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.prompt',
{ defaultMessage: 'Select or drag and drop the exported JSON file' }
);
export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.createSuccess',
{ defaultMessage: 'Macros uploaded successfully' }
);
export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.createError',
{ defaultMessage: 'Failed to upload macros file' }
);

View file

@ -6,25 +6,17 @@
*/
import type { EuiStepProps } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiStepNumber,
EuiSteps,
EuiTitle,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStepNumber, EuiTitle } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { SubStepsWrapper } from '../common/sub_step_wrapper';
import type { OnMigrationCreated, OnMissingResourcesFetched, DataInputStep } from '../../types';
import type { OnMigrationCreated, OnMissingResourcesFetched } from '../../types';
import * as i18n from './translations';
import { DataInputStep } from '../constants';
import { getStatus } from '../common/get_status';
import { SubSteps } from '../common/sub_step';
import { useCopyExportQueryStep } from './sub_steps/copy_export_query';
import { useRulesFileUploadStep } from './sub_steps/rules_file_upload';
import * as i18n from './translations';
import { useCheckResourcesStep } from './sub_steps/check_resources';
import { getStatus } from '../common/get_status';
const DataInputStepNumber: DataInputStep = 1;
interface RulesDataInputSubStepsProps {
migrationStats?: RuleMigrationTaskStats;
@ -37,7 +29,7 @@ interface RulesDataInputProps extends RulesDataInputSubStepsProps {
export const RulesDataInput = React.memo<RulesDataInputProps>(
({ dataInputStep, migrationStats, onMigrationCreated, onMissingResourcesFetched }) => {
const dataInputStatus = useMemo(
() => getStatus(DataInputStepNumber, dataInputStep),
() => getStatus(DataInputStep.Rules, dataInputStep),
[dataInputStep]
);
@ -49,7 +41,7 @@ export const RulesDataInput = React.memo<RulesDataInputProps>(
<EuiFlexItem grow={false}>
<EuiStepNumber
titleSize="xs"
number={DataInputStepNumber}
number={DataInputStep.Rules}
status={dataInputStatus}
/>
</EuiFlexItem>
@ -121,11 +113,7 @@ export const RulesDataInputSubSteps = React.memo<RulesDataInputSubStepsProps>(
[copyStep, uploadStep, resourcesStep]
);
return (
<SubStepsWrapper>
<EuiSteps titleSize="xxs" steps={steps} />
</SubStepsWrapper>
);
return <SubSteps steps={steps} />;
}
);
RulesDataInputSubSteps.displayName = 'RulesDataInputActive';

View file

@ -13,10 +13,3 @@ import type {
export type OnMigrationCreated = (migrationStats: RuleMigrationTaskStats) => void;
export type OnResourcesCreated = () => void;
export type OnMissingResourcesFetched = (missingResources: RuleMigrationResourceData[]) => void;
export enum DataInputStep {
Rules = 1,
Macros = 2,
Lookups = 3,
End = 10,
}

View file

@ -0,0 +1,86 @@
/*
* 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,
EuiFlexItem,
EuiText,
EuiPanel,
EuiProgress,
EuiLoadingSpinner,
EuiIcon,
EuiSpacer,
} from '@elastic/eui';
import { AssistantIcon } from '@kbn/ai-assistant-icon';
import { PanelText } from '../../../../common/components/panel_text';
import type { RuleMigrationStats } from '../../types';
import * as i18n from './translations';
import { RuleMigrationsReadMore } from './read_more';
export interface MigrationProgressPanelProps {
migrationStats: RuleMigrationStats;
}
export const MigrationProgressPanel = React.memo<MigrationProgressPanelProps>(
({ migrationStats }) => {
const finishedCount = migrationStats.rules.completed + migrationStats.rules.failed;
const progressValue = (finishedCount / migrationStats.rules.total) * 100;
const preparing = migrationStats.rules.pending === migrationStats.rules.total;
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<PanelText size="s" semiBold>
<p>{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}</p>
</PanelText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
{i18n.RULE_MIGRATION_PROGRESS_DESCRIPTION(migrationStats.rules.total)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup
direction="row"
justifyContent="flexStart"
alignItems="center"
gutterSize="s"
>
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={AssistantIcon} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<PanelText size="s" subdued>
{preparing ? i18n.RULE_MIGRATION_PREPARING : i18n.RULE_MIGRATION_TRANSLATING}
</PanelText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="s" />
</EuiFlexItem>
</EuiFlexGroup>
{!preparing && (
<>
<EuiProgress
value={progressValue}
valueText={`${Math.floor(progressValue)}%`}
max={100}
color="success"
/>
<EuiSpacer size="xs" />
<RuleMigrationsReadMore />
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
);
MigrationProgressPanel.displayName = 'MigrationProgressPanel';

View file

@ -0,0 +1,88 @@
/*
* 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 } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiPanel } from '@elastic/eui';
import { CenteredLoadingSpinner } from '../../../../common/components/centered_loading_spinner';
import type { RuleMigrationResourceData } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { PanelText } from '../../../../common/components/panel_text';
import { useStartMigration } from '../../service/hooks/use_start_migration';
import type { RuleMigrationStats } from '../../types';
import { useRuleMigrationDataInputContext } from '../data_input_flyout/context';
import * as i18n from './translations';
import { useGetMissingResources } from '../../service/hooks/use_get_missing_resources';
export interface MigrationReadyPanelProps {
migrationStats: RuleMigrationStats;
}
export const MigrationReadyPanel = React.memo<MigrationReadyPanelProps>(({ migrationStats }) => {
const { openFlyout } = useRuleMigrationDataInputContext();
const [missingResources, setMissingResources] = React.useState<RuleMigrationResourceData[]>([]);
const { getMissingResources, isLoading } = useGetMissingResources(setMissingResources);
useEffect(() => {
getMissingResources(migrationStats.id);
}, [getMissingResources, migrationStats.id]);
const onOpenFlyout = useCallback<React.MouseEventHandler>(() => {
openFlyout(migrationStats);
}, [openFlyout, migrationStats]);
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
<EuiFlexGroup direction="row" gutterSize="m" alignItems="flexEnd">
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<PanelText size="s" semiBold>
<p>{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}</p>
</PanelText>
</EuiFlexItem>
<EuiFlexItem>
<PanelText size="s" subdued>
{i18n.RULE_MIGRATION_READY_DESCRIPTION(
migrationStats.rules.total,
!isLoading && missingResources.length > 0
? i18n.RULE_MIGRATION_READY_MISSING_RESOURCES
: ''
)}
</PanelText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{isLoading ? (
<CenteredLoadingSpinner />
) : (
<EuiFlexItem grow={false}>
{missingResources.length > 0 ? (
<EuiButton fill iconType="download" iconSide="right" onClick={onOpenFlyout} size="s">
{i18n.RULE_MIGRATION_UPLOAD_BUTTON}
</EuiButton>
) : (
<StartTranslationButton migrationId={migrationStats.id} />
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPanel>
);
});
MigrationReadyPanel.displayName = 'MigrationReadyPanel';
const StartTranslationButton = React.memo<{ migrationId: string }>(({ migrationId }) => {
const { startMigration, isLoading } = useStartMigration();
const onStartMigration = useCallback(() => {
startMigration(migrationId);
}, [migrationId, startMigration]);
return (
<EuiButton fill onClick={onStartMigration} isLoading={isLoading} size="s">
{i18n.RULE_MIGRATION_START_TRANSLATION_BUTTON}
</EuiButton>
);
});
StartTranslationButton.displayName = 'StartTranslationButton';

View file

@ -0,0 +1,227 @@
/*
* 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 moment from 'moment';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiHorizontalRule,
EuiIcon,
EuiBasicTable,
EuiHealth,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { Chart, BarSeries, Settings, ScaleType, DARK_THEME, LIGHT_THEME } from '@elastic/charts';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { AssistantIcon } from '@kbn/ai-assistant-icon';
import { PanelText } from '../../../../common/components/panel_text';
import {
convertTranslationResultIntoText,
useResultVisColors,
} from '../../utils/translation_results';
import type { RuleMigrationTranslationStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { useGetMigrationTranslationStats } from '../../logic/use_get_migration_translation_stats';
import { CenteredLoadingSpinner } from '../../../../common/components/centered_loading_spinner';
import { SecuritySolutionLinkButton } from '../../../../common/components/links';
import type { RuleMigrationStats } from '../../types';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
export interface MigrationResultPanelProps {
migrationStats: RuleMigrationStats;
}
export const MigrationResultPanel = React.memo<MigrationResultPanelProps>(({ migrationStats }) => {
const { data: translationStats, isLoading: isLoadingTranslationStats } =
useGetMigrationTranslationStats(migrationStats.id);
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="none">
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m">
<EuiFlexGroup direction="column" alignItems="flexStart" gutterSize="xs">
<EuiFlexItem grow={false}>
<PanelText size="s" semiBold>
<p>{i18n.RULE_MIGRATION_COMPLETE_TITLE(migrationStats.number)}</p>
</PanelText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<PanelText size="s" subdued>
<p>
{i18n.RULE_MIGRATION_COMPLETE_DESCRIPTION(
moment(migrationStats.created_at).format('MMMM Do YYYY, h:mm:ss a'),
moment(migrationStats.last_updated_at).fromNow()
)}
</p>
</PanelText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiHorizontalRule margin="none" />
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m">
<EuiFlexGroup direction="column" alignItems="stretch" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type={AssistantIcon} size="m" />
</EuiFlexItem>
<EuiFlexItem>
<PanelText size="s" semiBold>
<p>{i18n.RULE_MIGRATION_SUMMARY_TITLE}</p>
</PanelText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
<EuiFlexGroup direction="column" alignItems="stretch" justifyContent="center">
<EuiFlexItem>
{isLoadingTranslationStats ? (
<CenteredLoadingSpinner />
) : (
translationStats && (
<>
<EuiText size="m" style={{ textAlign: 'center' }}>
<b>{i18n.RULE_MIGRATION_SUMMARY_CHART_TITLE}</b>
</EuiText>
<TranslationResultsChart translationStats={translationStats} />
<TranslationResultsTable translationStats={translationStats} />
</>
)
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" alignItems="center">
<EuiFlexItem>
<SecuritySolutionLinkButton
deepLinkId={SecurityPageName.siemMigrationsRules}
path={migrationStats.id}
>
{i18n.RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON}
</SecuritySolutionLinkButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
{/* TODO: uncomment when retry API is ready <RuleMigrationsUploadMissingPanel migrationStats={migrationStats} spacerSizeTop="s" /> */}
</EuiPanel>
</EuiPanel>
);
});
MigrationResultPanel.displayName = 'MigrationResultPanel';
const TranslationResultsChart = React.memo<{
translationStats: RuleMigrationTranslationStats;
}>(({ translationStats }) => {
const { colorMode } = useEuiTheme();
const translationResultColors = useResultVisColors();
const data = [
{
category: 'Results',
type: convertTranslationResultIntoText(RuleTranslationResult.FULL),
value: translationStats.rules.success.result.full,
},
{
category: 'Results',
type: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL),
value: translationStats.rules.success.result.partial,
},
{
category: 'Results',
type: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE),
value: translationStats.rules.success.result.untranslatable,
},
{
category: 'Results',
type: i18n.RULE_MIGRATION_TRANSLATION_FAILED,
value: translationStats.rules.failed,
},
];
const colors = [
translationResultColors[RuleTranslationResult.FULL],
translationResultColors[RuleTranslationResult.PARTIAL],
translationResultColors[RuleTranslationResult.UNTRANSLATABLE],
translationResultColors.error,
];
return (
<Chart size={{ height: 130 }}>
<Settings
showLegend={false}
rotation={90}
baseTheme={colorMode === 'DARK' ? DARK_THEME : LIGHT_THEME}
/>
<BarSeries
id="results"
name="Results"
data={data}
xAccessor="category"
yAccessors={['value']}
splitSeriesAccessors={['type']}
stackAccessors={['category']}
xScaleType={ScaleType.Ordinal}
yScaleType={ScaleType.Linear}
color={colors}
/>
</Chart>
);
});
TranslationResultsChart.displayName = 'TranslationResultsChart';
const TranslationResultsTable = React.memo<{
translationStats: RuleMigrationTranslationStats;
}>(({ translationStats }) => {
const translationResultColors = useResultVisColors();
const items = useMemo(() => {
return [
{
title: convertTranslationResultIntoText(RuleTranslationResult.FULL),
value: translationStats.rules.success.result.full,
color: translationResultColors[RuleTranslationResult.FULL],
},
{
title: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL),
value: translationStats.rules.success.result.partial,
color: translationResultColors[RuleTranslationResult.PARTIAL],
},
{
title: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE),
value: translationStats.rules.success.result.untranslatable,
color: translationResultColors[RuleTranslationResult.UNTRANSLATABLE],
},
{
title: i18n.RULE_MIGRATION_TRANSLATION_FAILED,
value: translationStats.rules.failed,
color: translationResultColors.error,
},
];
}, [translationStats, translationResultColors]);
return (
<EuiBasicTable
items={items}
compressed
columns={[
{
field: 'title',
name: i18n.RULE_MIGRATION_TABLE_COLUMN_RESULT,
render: (value: string, { color }) => <EuiHealth color={color}>{value}</EuiHealth>,
},
{
field: 'value',
name: i18n.RULE_MIGRATION_TABLE_COLUMN_RULES,
align: 'right',
},
]}
/>
);
});
TranslationResultsTable.displayName = 'TranslationResultsTable';

View file

@ -0,0 +1,37 @@
/*
* 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 { EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { PanelText } from '../../../../common/components/panel_text';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
export const RuleMigrationsReadMore = React.memo(() => {
const docLink = useKibana().services.docLinks.links.siem.gettingStarted;
return (
<PanelText size="xs" subdued>
<p>
<FormattedMessage
id="xpack.securitySolution.siemMigrations.rules.panel.help.readMore"
defaultMessage="Read more about our AI powered translations and other features. {readMore}"
values={{
readMore: (
<EuiLink href={docLink} target="_blank">
<FormattedMessage
id="xpack.securitySolution.siemMigrations.rules.panel.help.readDocs"
defaultMessage="Read AI docs"
/>
</EuiLink>
),
}}
/>
</p>
</PanelText>
);
});
RuleMigrationsReadMore.displayName = 'RuleMigrationsReadMore';

View file

@ -0,0 +1,101 @@
/*
* 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 RULE_MIGRATION_READY_DESCRIPTION = (
totalRules: number,
missingResourcesText: string
) =>
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.ready.description', {
defaultMessage:
'Migration of {totalRules} rules is created but the translation has not started yet. {missingResourcesText}',
values: { totalRules, missingResourcesText },
});
export const RULE_MIGRATION_READY_MISSING_RESOURCES = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.ready.missingResources',
{ defaultMessage: 'Upload macros & lookups and start the translation process' }
);
export const RULE_MIGRATION_START_TRANSLATION_BUTTON = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.translate.button',
{ defaultMessage: 'Start translation' }
);
export const RULE_MIGRATION_TITLE = (number: number) =>
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.migrationTitle', {
defaultMessage: 'SIEM rules migration #{number}',
values: { number },
});
export const RULE_MIGRATION_PROGRESS_DESCRIPTION = (totalRules: number) =>
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.progress.description', {
defaultMessage: `Processing migration of {totalRules} rules.`,
values: { totalRules },
});
export const RULE_MIGRATION_PREPARING = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.preparing',
{ defaultMessage: `Preparing environment for the AI powered translation.` }
);
export const RULE_MIGRATION_TRANSLATING = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.translating',
{ defaultMessage: `Translating rules` }
);
export const RULE_MIGRATION_COMPLETE_TITLE = (number: number) =>
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.result.title', {
defaultMessage: 'SIEM rules migration #{number} complete',
values: { number },
});
export const RULE_MIGRATION_COMPLETE_DESCRIPTION = (createdAt: string, finishedAt: string) =>
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.result.description', {
defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.',
values: { createdAt, finishedAt },
});
export const RULE_MIGRATION_SUMMARY_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.title',
{ defaultMessage: 'Translation Summary' }
);
export const RULE_MIGRATION_SUMMARY_CHART_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.chartTitle',
{ defaultMessage: 'Rules by translation status' }
);
export const RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.button',
{ defaultMessage: 'View translated rules' }
);
export const RULE_MIGRATION_TRANSLATION_FAILED = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.failed',
{ defaultMessage: 'Failed' }
);
export const RULE_MIGRATION_TABLE_COLUMN_RESULT = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.result',
{ defaultMessage: 'Result' }
);
export const RULE_MIGRATION_TABLE_COLUMN_RULES = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.rules',
{ defaultMessage: 'Rules' }
);
export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResources',
{ defaultMessage: 'Upload missing Macros and Lookups.' }
);
export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResourcesDescription',
{ defaultMessage: 'Click upload for step-by-step guidance to finish partially translated rules.' }
);
export const RULE_MIGRATION_UPLOAD_BUTTON = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.panel.uploadMacros.button',
{ defaultMessage: 'Upload' }
);

View file

@ -0,0 +1,89 @@
/*
* 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 } from 'react';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
useEuiTheme,
} from '@elastic/eui';
import { AssistantIcon } from '@kbn/ai-assistant-icon';
import type { SpacerSize } from '@elastic/eui/src/components/spacer/spacer';
import type { RuleMigrationResourceData } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { PanelText } from '../../../../common/components/panel_text';
import { useGetMissingResources } from '../../service/hooks/use_get_missing_resources';
import * as i18n from './translations';
import { useRuleMigrationDataInputContext } from '../data_input_flyout/context';
import type { RuleMigrationStats } from '../../types';
interface RuleMigrationsUploadMissingPanelProps {
migrationStats: RuleMigrationStats;
spacerSizeTop?: SpacerSize;
}
export const RuleMigrationsUploadMissingPanel = React.memo<RuleMigrationsUploadMissingPanelProps>(
({ migrationStats, spacerSizeTop }) => {
const { euiTheme } = useEuiTheme();
const { openFlyout } = useRuleMigrationDataInputContext();
const [missingResources, setMissingResources] = React.useState<RuleMigrationResourceData[]>([]);
const { getMissingResources, isLoading } = useGetMissingResources(setMissingResources);
useEffect(() => {
getMissingResources(migrationStats.id);
}, [getMissingResources, migrationStats.id]);
const onOpenFlyout = useCallback(() => {
openFlyout(migrationStats);
}, [migrationStats, openFlyout]);
if (isLoading || missingResources.length === 0) {
return null;
}
return (
<>
{spacerSizeTop && <EuiSpacer size={spacerSizeTop} />}
<EuiPanel
hasShadow={false}
hasBorder
paddingSize="s"
style={{ backgroundColor: euiTheme.colors.backgroundBasePrimary }}
>
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<AssistantIcon />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<PanelText size="s" semiBold>
{i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE}
</PanelText>
</EuiFlexItem>
<EuiFlexItem>
<PanelText size="s" subdued>
{i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION}
</PanelText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
color="primary"
onClick={onOpenFlyout}
iconType="download"
iconSide="right"
size="s"
>
{i18n.RULE_MIGRATION_UPLOAD_BUTTON}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</>
);
}
);
RuleMigrationsUploadMissingPanel.displayName = 'RuleMigrationsUploadMissingPanel';

View file

@ -26,7 +26,7 @@ import * as i18n from './translations';
import {
convertTranslationResultIntoColor,
convertTranslationResultIntoText,
} from '../../../../utils/helpers';
} from '../../../../utils/translation_results';
import { TranslationCallOut } from './callout';
interface TranslationTabProps {

View file

@ -31,7 +31,7 @@ import { useGetMigrationPrebuiltRules } from '../../logic/use_get_migration_preb
import * as logicI18n from '../../logic/translations';
import { BulkActions } from './bulk_actions';
import { SearchField } from './search_field';
import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
const DEFAULT_PAGE_SIZE = 10;
@ -80,10 +80,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
const tableSelection: EuiTableSelectionType<RuleMigration> = useMemo(
() => ({
selectable: (item: RuleMigration) => {
return (
!item.elastic_rule?.id &&
item.translation_result === SiemMigrationRuleTranslationResult.FULL
);
return !item.elastic_rule?.id && item.translation_result === RuleTranslationResult.FULL;
},
selectableMessage: (selectable: boolean, item: RuleMigration) => {
if (selectable) {
@ -190,7 +187,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
const canMigrationRuleBeInstalled =
!isLoading &&
!ruleMigration.elastic_rule?.id &&
ruleMigration.translation_result === SiemMigrationRuleTranslationResult.FULL;
ruleMigration.translation_result === RuleTranslationResult.FULL;
return (
<EuiFlexGroup>
<EuiFlexItem>
@ -271,7 +268,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
<EuiFlexItem grow={false}>
<BulkActions
isTableLoading={isLoading}
numberOfTranslatedRules={translationStats?.rules.installable ?? 0}
numberOfTranslatedRules={translationStats?.rules.success.installable ?? 0}
numberOfSelectedRules={selectedRuleMigrations.length}
installTranslatedRule={installTranslatedRules}
installSelectedRule={installSelectedRule}

View file

@ -6,16 +6,18 @@
*/
import React from 'react';
import { euiLightVars } from '@kbn/ui-theme';
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui';
import { css } from '@emotion/css';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import {
convertTranslationResultIntoText,
useResultVisColors,
} from '../../utils/translation_results';
import {
RuleMigrationStatusEnum,
type RuleMigration,
type RuleMigrationTranslationResult,
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { convertTranslationResultIntoText } from '../../utils/helpers';
import * as i18n from './translations';
const statusTextWrapperClassName = css`
@ -23,13 +25,6 @@ const statusTextWrapperClassName = css`
display: inline-grid;
`;
const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars;
const statusToColorMap: Record<RuleMigrationTranslationResult, string> = {
full: euiColorVis0,
partial: euiColorVis7,
untranslatable: euiColorVis9,
};
interface StatusBadgeProps {
migrationRule: RuleMigration;
'data-test-subj'?: string;
@ -37,13 +32,14 @@ interface StatusBadgeProps {
export const StatusBadge: React.FC<StatusBadgeProps> = React.memo(
({ migrationRule, 'data-test-subj': dataTestSubj = 'translation-result' }) => {
const colors = useResultVisColors();
// Installed
if (migrationRule.elastic_rule?.id) {
return (
<EuiToolTip content={i18n.RULE_STATUS_INSTALLED}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="check" color={statusToColorMap.full} />
<EuiIcon type="check" color={colors[RuleTranslationResult.FULL]} />
</EuiFlexItem>
<EuiFlexItem grow={false}>{i18n.RULE_STATUS_INSTALLED}</EuiFlexItem>
</EuiFlexGroup>
@ -67,7 +63,7 @@ export const StatusBadge: React.FC<StatusBadgeProps> = React.memo(
const translationResult = migrationRule.translation_result ?? 'untranslatable';
const displayValue = convertTranslationResultIntoText(translationResult);
const color = statusToColorMap[translationResult];
const color = colors[translationResult];
return (
<EuiToolTip content={displayValue}>

View file

@ -20,7 +20,7 @@ export type UpsertResources = (
migrationId: string,
data: UpsertRuleMigrationResourcesRequestBody
) => void;
export type OnSuccess = () => void;
export type OnSuccess = (data: UpsertRuleMigrationResourcesRequestBody) => void;
export const useUpsertResources = (onSuccess: OnSuccess) => {
const { siemMigrations, notifications } = useKibana().services;
@ -33,7 +33,7 @@ export const useUpsertResources = (onSuccess: OnSuccess) => {
dispatch({ type: 'start' });
await siemMigrations.rules.upsertMigrationResources(migrationId, data);
onSuccess();
onSuccess(data);
dispatch({ type: 'success' });
} catch (err) {
const apiError = err.body ?? err;

View file

@ -45,7 +45,7 @@ import * as i18n from './translations';
const NAMESPACE_TRACE_OPTIONS_SESSION_STORAGE_KEY =
`${DEFAULT_ASSISTANT_NAMESPACE}.${TRACE_OPTIONS_SESSION_STORAGE_KEY}` as const;
const REQUEST_POLLING_INTERVAL_MS = 5000 as const;
const REQUEST_POLLING_INTERVAL_SECONDS = 10 as const;
const CREATE_MIGRATION_BODY_BATCH_SIZE = 50 as const;
export class SiemRulesMigrationsService {
@ -213,7 +213,7 @@ export class SiemRulesMigrationsService {
}
}
await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_MS));
await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_SECONDS * 1000));
} while (pendingMigrationIds.length > 0);
}
}

View file

@ -1,44 +0,0 @@
/*
* 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 convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => {
switch (status) {
case RuleMigrationTranslationResultEnum.full:
return 'primary';
case RuleMigrationTranslationResultEnum.partial:
return 'warning';
case RuleMigrationTranslationResultEnum.untranslatable:
return 'danger';
default:
throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status));
}
};
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:
throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status));
}
};

View file

@ -0,0 +1,47 @@
/*
* 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 { useEuiTheme } from '@elastic/eui';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
export const useResultVisColors = () => {
const { euiTheme } = useEuiTheme();
return {
[RuleTranslationResult.FULL]: euiTheme.colors.vis.euiColorVis0,
[RuleTranslationResult.PARTIAL]: euiTheme.colors.vis.euiColorVis5,
[RuleTranslationResult.UNTRANSLATABLE]: euiTheme.colors.vis.euiColorVis7,
error: euiTheme.colors.vis.euiColorVis9,
};
};
export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => {
switch (status) {
case RuleTranslationResult.FULL:
return 'primary';
case RuleTranslationResult.PARTIAL:
return 'warning';
case RuleTranslationResult.UNTRANSLATABLE:
return 'danger';
default:
return 'subdued';
}
};
export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => {
switch (status) {
case RuleTranslationResult.FULL:
return i18n.SIEM_TRANSLATION_RESULT_FULL_LABEL;
case RuleTranslationResult.PARTIAL:
return i18n.SIEM_TRANSLATION_RESULT_PARTIAL_LABEL;
case RuleTranslationResult.UNTRANSLATABLE:
return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL;
default:
return i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL;
}
};

View file

@ -24,7 +24,7 @@ export const SIEM_TRANSLATION_RESULT_PARTIAL_LABEL = i18n.translate(
export const SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationResult.untranslatable',
{
defaultMessage: 'Not translated',
defaultMessage: 'Needs manual translation',
}
);

View file

@ -27,6 +27,7 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = (
path: SIEM_RULE_MIGRATION_RESOURCES_PATH,
access: 'internal',
security: { authz: { requiredPrivileges: ['securitySolution'] } },
options: { body: { maxBytes: 26214400 } }, // rise payload limit to 25MB
})
.addVersion(
{

View file

@ -41,6 +41,9 @@ export const registerSiemRuleMigrationsStatsRoute = (
const stats = await ruleMigrationsClient.task.getStats(migrationId);
if (stats.rules.total === 0) {
return res.noContent();
}
return res.ok({ body: stats });
} catch (err) {
logger.error(err);

View file

@ -45,6 +45,9 @@ export const registerSiemRuleMigrationsTranslationStatsRoute = (
const stats = await ruleMigrationsClient.data.rules.getTranslationStats(migrationId);
if (stats.rules.total === 0) {
return res.noContent();
}
return res.ok({ body: stats });
} catch (err) {
logger.error(err);

View file

@ -16,7 +16,10 @@ import type {
Duration,
} from '@elastic/elasticsearch/lib/api/types';
import type { StoredRuleMigration } from '../types';
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import {
SiemMigrationStatus,
RuleTranslationResult,
} from '../../../../../common/siem_migrations/constants';
import {
type RuleMigration,
type RuleMigrationTaskStats,
@ -128,19 +131,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
/** Retrieves an array of rule documents of a specific migrations */
async get(
migrationId: string,
{ filters = {}, sort = {}, from, size }: RuleMigrationGetOptions = {}
{ filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetOptions = {}
): Promise<{ total: number; data: StoredRuleMigration[] }> {
const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId, filters);
const sort = sortParam.sortField ? getSortingOptions(sortParam) : undefined;
const result = await this.esClient
.search<RuleMigration>({
index,
query,
sort: sort.sortField ? getSortingOptions(sort) : undefined,
from,
size,
})
.search<RuleMigration>({ index, query, sort, from, size })
.catch((error) => {
this.logger.error(`Error searching rule migrations: ${error.message}`);
throw error;
@ -268,8 +266,15 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
const query = this.getFilterQuery(migrationId);
const aggregations = {
prebuilt: { filter: searchConditions.isPrebuilt() },
installable: { filter: { bool: { must: searchConditions.isInstallable() } } },
success: {
filter: { term: { status: SiemMigrationStatus.COMPLETED } },
aggs: {
result: { terms: { field: 'translation_result' } },
installable: { filter: { bool: { must: searchConditions.isInstallable() } } },
prebuilt: { filter: searchConditions.isPrebuilt() },
},
},
failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } },
};
const result = await this.esClient
.search({ index, query, aggregations, _source: false })
@ -278,16 +283,22 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
throw error;
});
const bucket = result.aggregations ?? {};
const aggs = result.aggregations ?? {};
const total = this.getTotalHits(result);
const prebuilt = (bucket.prebuilt as AggregationsFilterAggregate)?.doc_count ?? 0;
const successAgg = aggs.success as AggregationsFilterAggregate;
const translationResultsAgg = successAgg.result as AggregationsStringTermsAggregate;
return {
id: migrationId,
rules: {
total,
prebuilt,
custom: total - prebuilt,
installable: (bucket.installable as AggregationsFilterAggregate)?.doc_count ?? 0,
success: {
total: (successAgg as AggregationsFilterAggregate)?.doc_count ?? 0,
result: this.translationResultAggCount(translationResultsAgg),
installable: (successAgg.installable as AggregationsFilterAggregate)?.doc_count ?? 0,
prebuilt: (successAgg.prebuilt as AggregationsFilterAggregate)?.doc_count ?? 0,
},
failed: (aggs.failed as AggregationsFilterAggregate)?.doc_count ?? 0,
},
};
}
@ -297,10 +308,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId);
const aggregations = {
pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } },
processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } },
completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } },
failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } },
status: { terms: { field: 'status' } },
createdAt: { min: { field: '@timestamp' } },
lastUpdatedAt: { max: { field: 'updated_at' } },
};
@ -311,18 +319,16 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
throw error;
});
const bucket = result.aggregations ?? {};
const aggs = result.aggregations ?? {};
return {
id: migrationId,
rules: {
total: this.getTotalHits(result),
pending: (bucket.pending as AggregationsFilterAggregate)?.doc_count ?? 0,
processing: (bucket.processing as AggregationsFilterAggregate)?.doc_count ?? 0,
completed: (bucket.completed as AggregationsFilterAggregate)?.doc_count ?? 0,
failed: (bucket.failed as AggregationsFilterAggregate)?.doc_count ?? 0,
...this.statusAggCounts(aggs.status as AggregationsStringTermsAggregate),
},
created_at: (bucket.createdAt as AggregationsMinAggregate)?.value_as_string ?? '',
last_updated_at: (bucket.lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string ?? '',
created_at: (aggs.createdAt as AggregationsMinAggregate)?.value_as_string ?? '',
last_updated_at: (aggs.lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string ?? '',
};
}
@ -331,12 +337,9 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
const index = await this.getIndexName();
const aggregations: { migrationIds: AggregationsAggregationContainer } = {
migrationIds: {
terms: { field: 'migration_id', order: { createdAt: 'asc' } },
terms: { field: 'migration_id', order: { createdAt: 'asc' }, size: 10000 },
aggregations: {
pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } },
processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } },
completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } },
failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } },
status: { terms: { field: 'status' } },
createdAt: { min: { field: '@timestamp' } },
lastUpdatedAt: { max: { field: 'updated_at' } },
},
@ -355,16 +358,43 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
id: bucket.key,
rules: {
total: bucket.doc_count,
pending: bucket.pending?.doc_count ?? 0,
processing: bucket.processing?.doc_count ?? 0,
completed: bucket.completed?.doc_count ?? 0,
failed: bucket.failed?.doc_count ?? 0,
...this.statusAggCounts(bucket.status),
},
created_at: bucket.createdAt?.value_as_string,
last_updated_at: bucket.lastUpdatedAt?.value_as_string,
}));
}
private statusAggCounts(
statusAgg: AggregationsStringTermsAggregate
): Record<SiemMigrationStatus, number> {
const buckets = statusAgg.buckets as AggregationsStringTermsBucket[];
return {
[SiemMigrationStatus.PENDING]:
buckets.find(({ key }) => key === SiemMigrationStatus.PENDING)?.doc_count ?? 0,
[SiemMigrationStatus.PROCESSING]:
buckets.find(({ key }) => key === SiemMigrationStatus.PROCESSING)?.doc_count ?? 0,
[SiemMigrationStatus.COMPLETED]:
buckets.find(({ key }) => key === SiemMigrationStatus.COMPLETED)?.doc_count ?? 0,
[SiemMigrationStatus.FAILED]:
buckets.find(({ key }) => key === SiemMigrationStatus.FAILED)?.doc_count ?? 0,
};
}
private translationResultAggCount(
resultAgg: AggregationsStringTermsAggregate
): Record<RuleTranslationResult, number> {
const buckets = resultAgg.buckets as AggregationsStringTermsBucket[];
return {
[RuleTranslationResult.FULL]:
buckets.find(({ key }) => key === RuleTranslationResult.FULL)?.doc_count ?? 0,
[RuleTranslationResult.PARTIAL]:
buckets.find(({ key }) => key === RuleTranslationResult.PARTIAL)?.doc_count ?? 0,
[RuleTranslationResult.UNTRANSLATABLE]:
buckets.find(({ key }) => key === RuleTranslationResult.UNTRANSLATABLE)?.doc_count ?? 0,
};
}
private getFilterQuery(
migrationId: string,
{ status, ids, installable, prebuilt, searchTerm }: RuleMigrationFilters = {}

View file

@ -6,11 +6,11 @@
*/
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
export const conditions = {
isFullyTranslated(): QueryDslQueryContainer {
return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } };
return { term: { translation_result: RuleTranslationResult.FULL } };
},
isNotInstalled(): QueryDslQueryContainer {
return {

View file

@ -6,7 +6,7 @@
*/
import { END, START, StateGraph } from '@langchain/langgraph';
import { SiemMigrationRuleTranslationResult } from '../../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../../common/siem_migrations/constants';
import { getCreateSemanticQueryNode } from './nodes/create_semantic_query';
import { getMatchPrebuiltRuleNode } from './nodes/match_prebuilt_rule';
import { getProcessQueryNode } from './nodes/process_query';
@ -62,7 +62,7 @@ const matchedPrebuiltRuleConditional = (state: MigrateRuleState) => {
if (state.elastic_rule?.prebuilt_rule_id) {
return END;
}
if (state.translation_result === SiemMigrationRuleTranslationResult.UNTRANSLATABLE) {
if (state.translation_result === RuleTranslationResult.UNTRANSLATABLE) {
return END;
}
return 'processQuery';

View file

@ -7,7 +7,7 @@
import type { Logger } from '@kbn/core/server';
import { JsonOutputParser } from '@langchain/core/output_parsers';
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
import type { RuleMigrationsRetriever } from '../../../retrievers';
import type { ChatModel } from '../../../util/actions_client_chat';
import type { GraphNode } from '../../types';
@ -69,7 +69,7 @@ export const getMatchPrebuiltRuleNode = ({
id: matchedRule.installedRuleId,
prebuilt_rule_id: matchedRule.rule_id,
},
translation_result: SiemMigrationRuleTranslationResult.FULL,
translation_result: RuleTranslationResult.FULL,
};
}
}
@ -81,7 +81,7 @@ export const getMatchPrebuiltRuleNode = ({
logger.debug(
`Rule: ${state.original_rule?.title} did not match any prebuilt rule, but contains inputlookup, dropping`
);
return { translation_result: SiemMigrationRuleTranslationResult.UNTRANSLATABLE };
return { translation_result: RuleTranslationResult.UNTRANSLATABLE };
}
return {};
};

View file

@ -7,7 +7,7 @@
import type { BaseMessage } from '@langchain/core/messages';
import { Annotation, messagesStateReducer } from '@langchain/langgraph';
import type { SiemMigrationRuleTranslationResult } from '../../../../../../common/siem_migrations/constants';
import type { RuleTranslationResult } from '../../../../../../common/siem_migrations/constants';
import type {
ElasticRule,
OriginalRule,
@ -31,7 +31,7 @@ export const migrateRuleState = Annotation.Root({
reducer: (current, value) => value ?? current,
default: () => '',
}),
translation_result: Annotation<SiemMigrationRuleTranslationResult>(),
translation_result: Annotation<RuleTranslationResult>(),
comments: Annotation<RuleMigration['comments']>({
reducer: (current, value) => (value ? (current ?? []).concat(value) : current),
default: () => [],

View file

@ -7,7 +7,7 @@
import { END, START, StateGraph } from '@langchain/langgraph';
import { isEmpty } from 'lodash/fp';
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
import { getEcsMappingNode } from './nodes/ecs_mapping';
import { getFilterIndexPatternsNode } from './nodes/filter_index_patterns';
import { getFixQueryErrorsNode } from './nodes/fix_query_errors';
@ -67,7 +67,7 @@ export function getTranslateRuleGraph({
const validationRouter = (state: TranslateRuleState) => {
if (
state.validation_errors.iterations <= MAX_VALIDATION_ITERATIONS &&
state.translation_result === SiemMigrationRuleTranslationResult.FULL
state.translation_result === RuleTranslationResult.FULL
) {
if (!isEmpty(state.validation_errors?.esql_errors)) {
return 'fixQueryErrors';

View file

@ -7,7 +7,7 @@
import type { Logger } from '@kbn/core/server';
import type { InferenceClient } from '@kbn/inference-plugin/server';
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
import { getEsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base_caller';
import type { GraphNode } from '../../types';
import { SIEM_RULE_MIGRATION_CIM_ECS_MAP } from './cim_ecs_map';
@ -58,9 +58,9 @@ export const getEcsMappingNode = ({
};
};
const getTranslationResult = (esqlQuery: string): SiemMigrationRuleTranslationResult => {
const getTranslationResult = (esqlQuery: string): RuleTranslationResult => {
if (esqlQuery.match(/\[(macro):[\s\S]*\]/)) {
return SiemMigrationRuleTranslationResult.PARTIAL;
return RuleTranslationResult.PARTIAL;
}
return SiemMigrationRuleTranslationResult.FULL;
return RuleTranslationResult.FULL;
};

View file

@ -6,7 +6,7 @@
*/
import type { Logger } from '@kbn/core/server';
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
import type { GraphNode } from '../../types';
interface GetFilterIndexPatternsNodeParams {
@ -30,7 +30,7 @@ export const getFilterIndexPatternsNode = ({
elastic_rule: {
...state.elastic_rule,
query: newQuery,
translation_result: SiemMigrationRuleTranslationResult.PARTIAL,
translation_result: RuleTranslationResult.PARTIAL,
},
};
}

View file

@ -7,7 +7,7 @@
import type { Logger } from '@kbn/core/server';
import type { InferenceClient } from '@kbn/inference-plugin/server';
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
import { getEsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base_caller';
import type { GraphNode } from '../../types';
import { ESQL_SYNTAX_TRANSLATION_PROMPT } from './prompts';
@ -63,9 +63,9 @@ export const getTranslateRuleNode = ({
};
};
const getTranslationResult = (esqlQuery: string): SiemMigrationRuleTranslationResult => {
const getTranslationResult = (esqlQuery: string): RuleTranslationResult => {
if (esqlQuery.match(/\[(macro):[\s\S]*\]/)) {
return SiemMigrationRuleTranslationResult.PARTIAL;
return RuleTranslationResult.PARTIAL;
}
return SiemMigrationRuleTranslationResult.FULL;
return RuleTranslationResult.FULL;
};

View file

@ -7,7 +7,7 @@
import type { BaseMessage } from '@langchain/core/messages';
import { Annotation, messagesStateReducer } from '@langchain/langgraph';
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
import type {
ElasticRule,
OriginalRule,
@ -46,9 +46,9 @@ export const translateRuleState = Annotation.Root({
reducer: (current, value) => value ?? current,
default: () => ({ iterations: 0 } as TranslateRuleValidationErrors),
}),
translation_result: Annotation<SiemMigrationRuleTranslationResult>({
translation_result: Annotation<RuleTranslationResult>({
reducer: (current, value) => value ?? current,
default: () => SiemMigrationRuleTranslationResult.UNTRANSLATABLE,
default: () => RuleTranslationResult.UNTRANSLATABLE,
}),
comments: Annotation<RuleMigration['comments']>({
reducer: (current, value) => (value ? (current ?? []).concat(value) : current),