Skip to content

Commit

Permalink
[Security Solution] Display unavailable ML jobs (#205483)
Browse files Browse the repository at this point in the history
**Resolves: #202700

## Summary
This PR resolves an issue where an ML job referenced by a rule does not
appear in the Rule Details, Upgrade flyout, or Rule Editing pages if the
job is missing or not yet created.

For example, if you had an ML rule with a single selected job and this
job was not available, you would see a blank space instead of job name.

## Screenshots
**Rule Details: Before**
<img width="578" alt="Scherm­afbeelding 2025-01-03 om 13 20 32"
src="https://github.com/user-attachments/assets/e8bc073f-0420-4888-8dd9-b4dc70fd0682"
/>

**Rule Details: After**
<img width="578" alt="Scherm­afbeelding 2025-01-03 om 13 20 05"
src="https://github.com/user-attachments/assets/bd4d0f91-8adf-45c5-8d31-b42ac483027b"
/>

**Rule Edit: Before**
<img width="427" alt="Scherm­afbeelding 2025-01-03 om 13 21 21"
src="https://github.com/user-attachments/assets/bffcb871-8cfc-4f50-8d19-c14b122c0be4"
/>

**Rule Edit: After**
<img width="427" alt="Scherm­afbeelding 2025-01-03 om 13 21 09"
src="https://github.com/user-attachments/assets/be8f60b9-17a6-48d2-978c-cfa63c426a08"
/>

**Upgrade flyout: Before**
<img width="1066" alt="Scherm­afbeelding 2025-01-03 om 13 22 30"
src="https://github.com/user-attachments/assets/553ff837-95cf-4670-91f1-dffb169ec505"
/>

**Upgrade flyout: After**
<img width="1066" alt="Scherm­afbeelding 2025-01-03 om 13 21 55"
src="https://github.com/user-attachments/assets/150cfb82-bc69-4aeb-a20a-03f54c7edc70"
/>

## Testing
You can test by removing an ML job referenced by a rule in
`http://localhost:<port>/kbn/app/ml/jobs`.
  • Loading branch information
nikitaindik authored Jan 6, 2025
1 parent 4eb9006 commit e04b200
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,6 @@
"avcBanner.body": "Elastic Security passe avec brio le test de protection contre les malwares réalisé par AV-Comparatives",
"avcBanner.readTheBlog.link": "Lire le blog",
"avcBanner.title": "Protection à 100 % sans aucun faux positif.",
"bfetch.advancedSettings.disableBfetchCompressionDeprecation": "Ce paramètre est déclassé et sera supprimé dans la version 9.0 de Kibana.",
"bfetch.advancedSettings.disableBfetchDeprecation": "Ce paramètre est déclassé et sera supprimé dans la version 9.0 de Kibana.",
"bfetch.disableBfetch": "Désactiver la mise en lots de requêtes",
"bfetch.disableBfetchCompression": "Désactiver la compression par lots",
"bfetch.disableBfetchCompressionDesc": "Vous pouvez désactiver la compression par lots. Cela permet de déboguer des requêtes individuelles, mais augmente la taille des réponses.",
"bfetch.disableBfetchDesc": "Désactive la mise en lot des requêtes. Cette option augmente le nombre de requêtes HTTP depuis Kibana, mais permet de les déboguer individuellement.",
"bfetchError.networkError": "Vérifiez votre connexion réseau et réessayez.",
"bfetchError.networkErrorWithStatus": "Vérifiez votre connexion réseau et réessayez. Code {code}",
"cases.components.status.closed": "Fermé",
"cases.components.status.inProgress": "En cours",
"cases.components.status.open": "Ouvrir",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,6 @@
"avcBanner.body": "AV-Comparativesのマルウェア保護テストで高い評価を受けたElastic Security",
"avcBanner.readTheBlog.link": "ブログを読む",
"avcBanner.title": "誤検知がゼロの100%保護。",
"bfetch.advancedSettings.disableBfetchCompressionDeprecation": "この設定はサポートが終了し、Kibana 9.0では削除されます。",
"bfetch.advancedSettings.disableBfetchDeprecation": "この設定はサポートが終了し、Kibana 9.0では削除されます。",
"bfetch.disableBfetch": "リクエストバッチを無効にする",
"bfetch.disableBfetchCompression": "バッチ圧縮を無効にする",
"bfetch.disableBfetchCompressionDesc": "バッチ圧縮を無効にします。個別の要求をデバッグできますが、応答サイズが大きくなります。",
"bfetch.disableBfetchDesc": "リクエストバッチを無効にします。これにより、KibanaからのHTTPリクエスト数は減りますが、個別にリクエストをデバッグできます。",
"bfetchError.networkError": "ネットワーク接続を確認して再試行してください。",
"bfetchError.networkErrorWithStatus": "ネットワーク接続を確認して再試行してください。コード{code}",
"cases.components.status.closed": "終了",
"cases.components.status.inProgress": "進行中",
"cases.components.status.open": "オープン",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,6 @@
"avcBanner.body": "在 AV-Comparatives 进行的恶意软件防护测试中,Elastic Security 表现突出",
"avcBanner.readTheBlog.link": "阅读博客",
"avcBanner.title": "提供全面保护,误报率为零。",
"bfetch.advancedSettings.disableBfetchCompressionDeprecation": "此设置已过时,将在 Kibana 9.0 中移除。",
"bfetch.advancedSettings.disableBfetchDeprecation": "此设置已过时,将在 Kibana 9.0 中移除。",
"bfetch.disableBfetch": "禁用请求批处理",
"bfetch.disableBfetchCompression": "禁用批量压缩",
"bfetch.disableBfetchCompressionDesc": "禁用批量压缩。这允许您对单个请求进行故障排查,但会增加响应大小。",
"bfetch.disableBfetchDesc": "禁用请求批处理。这会增加来自 Kibana 的 HTTP 请求数,但允许对单个请求进行故障排查。",
"bfetchError.networkError": "检查您的网络连接,然后重试。",
"bfetchError.networkErrorWithStatus": "检查您的网络连接,然后重试。代码 {code}",
"cases.components.status.closed": "已关闭",
"cases.components.status.inProgress": "进行中",
"cases.components.status.open": "打开",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('MlJobSelect', () => {

it('renders correctly', () => {
const Component = () => {
const field = useFormFieldMock<string[]>();
const field = useFormFieldMock<string[]>({ value: [] });

return <MlJobSelect field={field} loading={false} jobs={[]} />;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ export const MlJobSelect: React.FC<MlJobSelectProps> = ({
label: `${job.customSettings?.security_app_display_name} ${job.id}`,
}));

// If rule's ML job is no longer available or has not yet become available, we still want it to appear in the dropdown.
selectedJobIds.forEach((selectedJobId) => {
const isSelectedJobAvailable = jobOptions.some((job) => job.value.id === selectedJobId);
if (!isSelectedJobAvailable) {
jobOptions.push({
value: {
id: selectedJobId,
description: '',
name: selectedJobId,
},
label: selectedJobId,
});
}
});

const selectedJobOptions = jobOptions
.filter((option) => selectedJobIds.includes(option.value.id))
// 'label' defines what is rendered inside the selected ComboBoxPill
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { FC, ReactNode } from 'react';
import React, { memo } from 'react';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';

import type { MlSummaryJob } from '@kbn/ml-plugin/public';
import * as i18n from './translations';
Expand All @@ -21,6 +22,7 @@ import { MlJobStatusBadge } from '../ml_job_status_badge';

const Wrapper = styled.div`
overflow: hidden;
margin-bottom: ${euiThemeVars.euiSizeS};
`;

const MlJobItemComponent: FC<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
EuiFlexItem,
EuiFlexGroup,
EuiLoadingSpinner,
EuiButtonIcon,
EuiPopover,
} from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui';
import type {
Expand All @@ -24,6 +26,7 @@ import type { Filter } from '@kbn/es-query';
import type { SavedQuery } from '@kbn/data-plugin/public';
import { mapAndFlattenFilters } from '@kbn/data-plugin/public';
import { FilterItems } from '@kbn/unified-search-plugin/public';
import useToggle from 'react-use/lib/useToggle';
import { isDataView } from '../../../../common/components/query_bar';
import type {
AlertSuppressionMissingFieldsStrategy,
Expand Down Expand Up @@ -237,19 +240,28 @@ interface MachineLearningJobListProps {
}

export const MachineLearningJobList = ({ jobIds, isInteractive }: MachineLearningJobListProps) => {
const { jobs } = useSecurityJobs();
const { jobs: availableJobs } = useSecurityJobs();

if (!jobIds) {
return null;
}

const jobIdsArray = Array.isArray(jobIds) ? jobIds : [jobIds];

const unavailableJobIds = jobIdsArray.filter(
(jobId) => !availableJobs.some((job) => job.id === jobId)
);

if (isInteractive) {
return <MlJobsDescription jobIds={jobIdsArray} />;
return (
<>
<MlJobsDescription jobIds={jobIdsArray} />
<UnavailableMlJobs unavailableJobIds={unavailableJobIds} />
</>
);
}

const relevantJobs = jobs.filter((job) => jobIdsArray.includes(job.id));
const relevantJobs = availableJobs.filter((job) => jobIdsArray.includes(job.id));

return (
<>
Expand All @@ -260,10 +272,48 @@ export const MachineLearningJobList = ({ jobIds, isInteractive }: MachineLearnin
jobName={job.customSettings?.security_app_display_name}
/>
))}
<UnavailableMlJobs unavailableJobIds={unavailableJobIds} />
</>
);
};

interface UnavailableMlJobsProps {
unavailableJobIds: string[];
}

const UnavailableMlJobs = ({ unavailableJobIds }: UnavailableMlJobsProps) => {
return unavailableJobIds.map((jobId) => (
<div key={jobId}>
<UnavailableMlJobLink jobId={jobId} />
</div>
));
};

interface UnavailableMlJobLinkProps {
jobId: string;
}

const UnavailableMlJobLink: React.FC<UnavailableMlJobLinkProps> = ({ jobId }) => {
const [isPopoverOpen, togglePopover] = useToggle(false);

const button = (
<EuiButtonIcon
iconType="questionInCircle"
onClick={togglePopover}
aria-label={i18n.MACHINE_LEARNING_JOB_NOT_AVAILABLE}
/>
);

return (
<EuiText component="span" color="subdued" size="s">
{jobId}
<EuiPopover button={button} isOpen={isPopoverOpen} closePopover={togglePopover}>
{i18n.MACHINE_LEARNING_JOB_NOT_AVAILABLE}
</EuiPopover>
</EuiText>
);
};

const getRuleTypeDescription = (ruleType: Type) => {
switch (ruleType) {
case 'machine_learning':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,21 @@ export const MACHINE_LEARNING_JOB_ID_FIELD_LABEL = i18n.translate(
}
);

export const MACHINE_LEARNING_JOB_NOT_AVAILABLE = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.machineLearning.mlJobNotAvailable',
{
defaultMessage:
'This job is currently unavailable. Please ensure that all related ML integrations are installed and configured.',
}
);

export const OPEN_HELP_POPOVER_ARIA_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.machineLearning.mlJobNotAvailable.openHelpPopoverAriaLabel',
{
defaultMessage: 'Open help popover',
}
);

export const ANOMALY_THRESHOLD_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.anomalyThresholdFieldLabel',
{
Expand Down

0 comments on commit e04b200

Please sign in to comment.