Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Security Solution] Add threshold, machine_learning_job_id and anomaly_threshold editable fields #200323

Merged
merged 49 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
11fe8b5
Add `machine_learning_job_id`
nikitaindik Nov 16, 2024
d57358b
Add `anomaly_threshold`
nikitaindik Nov 17, 2024
6f74bda
Add `threshold`
nikitaindik Nov 26, 2024
32b5477
Fix i18n
nikitaindik Nov 26, 2024
b6a9d92
Remove unused import
nikitaindik Nov 26, 2024
cc72aa9
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Nov 27, 2024
9991cbd
Fix failing test
nikitaindik Nov 27, 2024
65e5266
Fix failing Jest tests
nikitaindik Nov 28, 2024
ef52eb1
Move `anomaly_threshold_edit.tsx` outside directory
nikitaindik Nov 28, 2024
2ef3c64
Simplify `componentProps` in `MachineLearningJobSelector`
nikitaindik Nov 28, 2024
7101058
Simplify validations
nikitaindik Nov 28, 2024
19be452
Use `??` instead of `||`
nikitaindik Nov 28, 2024
0e60504
Simplify `threshold` serialization and deserialization
nikitaindik Nov 28, 2024
5e7fd29
Add `assertUnreachable` to `ThresholdRuleFieldEdit`
nikitaindik Nov 28, 2024
ca03da3
Use `??` instead of `||` in `ThresholdAdapter`
nikitaindik Nov 28, 2024
f0b7db6
Remove redundant serializers and deserializers
nikitaindik Nov 28, 2024
27bc329
Refactor Machine Learning Job ID-related components
nikitaindik Nov 30, 2024
dc785d7
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Nov 30, 2024
a9467b4
Remove `thresholdFields` from `StepDefineFieldsProps`
nikitaindik Nov 30, 2024
d161bf8
Fix failing ML HelpText test
nikitaindik Nov 30, 2024
a2b3c53
Make TS happy
nikitaindik Dec 1, 2024
3d56412
Use container queries instead of `EuiAutoSizer` for responsive layout
nikitaindik Dec 1, 2024
f4907b4
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Dec 1, 2024
350ad19
Make validation work for ML job ID and move field config into edit co…
nikitaindik Dec 3, 2024
664b5a4
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Dec 3, 2024
72b195a
Fix displaying long field names in Threshold input
nikitaindik Dec 3, 2024
068f879
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Dec 4, 2024
9e231bb
Move "Create custom job" button margin outside of component
nikitaindik Dec 4, 2024
0933e39
Move aggregatable fields computation into `ThresholdEdit`
nikitaindik Dec 4, 2024
318b023
Merge branch 'main' into machine-learning-fields
nikitaindik Dec 5, 2024
26e947a
Merge branch 'main' into machine-learning-fields
nikitaindik Dec 6, 2024
a4e0c6e
Merge branch 'main' into machine-learning-fields
nikitaindik Dec 9, 2024
46daae5
Merge branch 'main' into machine-learning-fields
nikitaindik Dec 10, 2024
f30cb6d
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Dec 11, 2024
c360516
Merge branch 'main' into machine-learning-fields
nikitaindik Dec 14, 2024
c175679
Merge branch 'main' into machine-learning-fields
nikitaindik Dec 16, 2024
0993cc0
Update imports to reflect latest structure changes
nikitaindik Dec 16, 2024
8a05118
Make sure alert suppression component updates when Threshold "Group b…
nikitaindik Dec 16, 2024
db6e424
Update `>=` operator styles
nikitaindik Dec 16, 2024
7fa5e9f
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Dec 17, 2024
0e07340
Move files to match Sustainable Kibana Arch PR
nikitaindik Dec 17, 2024
349f448
Delete duplicated test file
nikitaindik Dec 17, 2024
0042063
Get rid of a custom margin in favour of EuiSpacer
nikitaindik Dec 17, 2024
1014b62
Remove unused import
nikitaindik Dec 17, 2024
8c786d5
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Dec 18, 2024
dc59c29
Update styles
nikitaindik Dec 18, 2024
f1de5b6
Used fixed width for number inputs
nikitaindik Dec 18, 2024
468a083
Update spacing for ML job creation button
nikitaindik Dec 18, 2024
0975e4a
Merge remote-tracking branch 'upstream/main' into machine-learning-fi…
nikitaindik Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { MachineLearningJobId } from '../../../common/api/detection_engine';

export function normalizeMachineLearningJobId(jobId: MachineLearningJobId): string[] {
return typeof jobId === 'string' ? [jobId] : jobId;
}
Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import { UseField } from '../../../../shared_imports';
import { AnomalyThresholdSlider } from '../../../rule_creation_ui/components/anomaly_threshold_slider';
import * as i18n from './translations';

const componentProps = {
describedByIds: ['anomalyThreshold'],
};

interface AnomalyThresholdEditProps {
path: string;
}

export function AnomalyThresholdEdit({ path }: AnomalyThresholdEditProps): JSX.Element {
return (
<UseField
path={path}
config={ANOMALY_THRESHOLD_FIELD_CONFIG}
component={AnomalyThresholdSlider}
componentProps={componentProps}
/>
);
}

const ANOMALY_THRESHOLD_FIELD_CONFIG = {
label: i18n.ANOMALY_THRESHOLD_LABEL,
};
Original file line number Diff line number Diff line change
@@ -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 { AnomalyThresholdEdit } from './anomaly_threshold_edit';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

export const ANOMALY_THRESHOLD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel',
{
defaultMessage: 'Anomaly score threshold',
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { css } from '@emotion/css';
import { EuiButton } from '@elastic/eui';
import { useKibana } from '../../../../common/lib/kibana';
import * as i18n from './translations';

const buttonClassName = css`
margin-top: 20px;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number? Should we be referencing a theme variable instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need 20px here to make the button flush with the dropdown. euiSizeM is 16px, euiSizeL is 24px.

Scherm­afbeelding 2024-12-04 om 13 17 55

But now as you pointed this out, I think the margin should live outside of this component since it's related to an external thing.

`;

export function CreateCustomMlJobButton(): JSX.Element {
const { navigateToApp } = useKibana().services.application;

return (
<EuiButton
className={buttonClassName}
iconType="popout"
iconSide="right"
onClick={() => navigateToApp('ml', { openInNewTab: true })}
>
{i18n.CREATE_CUSTOM_JOB_BUTTON_TITLE}
</EuiButton>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

export const CREATE_CUSTOM_JOB_BUTTON_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.mlSelectJob.createCustomJobButtonTitle',
{
defaultMessage: 'Create custom job',
}
);
Original file line number Diff line number Diff line change
@@ -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 { MachineLearningJobIdEdit } from './machine_learning_job_id_edit';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useMemo } from 'react';
import { UseField, fieldValidators } from '../../../../shared_imports';
import { MlJobSelect } from '../ml_job_select';
nikitaindik marked this conversation as resolved.
Show resolved Hide resolved
import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs';
import * as i18n from './translations';

interface MachineLearningJobIdEditProps {
path: string;
shouldShowHelpText?: boolean;
}

export function MachineLearningJobIdEdit({
path,
shouldShowHelpText,
}: MachineLearningJobIdEditProps): JSX.Element {
const { loading, jobs } = useSecurityJobs();

const componentProps = useMemo(
() => ({
jobs,
loading,
shouldShowHelpText,
}),
[jobs, loading, shouldShowHelpText]
);

return (
<UseField
path={path}
config={MACHINE_LEARNING_JOB_ID_FIELD_CONFIG}
component={MlJobSelect}
componentProps={componentProps}
/>
);
}

const MACHINE_LEARNING_JOB_ID_FIELD_CONFIG = {
label: i18n.MACHINE_LEARNING_JOB_ID_LABEL,
validations: [
{
validator: fieldValidators.emptyField(
i18n.MACHINE_LEARNING_JOB_ID_EMPTY_FIELD_VALIDATION_ERROR
),
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 MACHINE_LEARNING_JOB_ID_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel',
{
defaultMessage: 'Machine Learning job',
}
);

export const MACHINE_LEARNING_JOB_ID_EMPTY_FIELD_VALIDATION_ERROR = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired',
{
defaultMessage: 'A Machine Learning job is required.',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,46 @@
import React from 'react';
import { shallow } from 'enzyme';
import { HelpText } from './help_text';
import type { SecurityJob } from '../../../../common/components/ml_popover/types';

jest.mock('../../../../common/lib/kibana', () => {
return {
useKibana: jest.fn().mockReturnValue({
services: {
application: {
getUrlForApp: () => '/app/ml',
},
},
}),
};
});

describe('MlJobSelect help text', () => {
it('does not show warning if all jobs are running', () => {
const wrapper = shallow(<HelpText href={'https://test.com'} notRunningJobIds={[]} />);
const jobs = [
{
id: 'test-id',
jobState: 'opened',
datafeedState: 'opened',
},
] as SecurityJob[];
const selectedJobIds = ['test-id'];

const wrapper = shallow(<HelpText jobs={jobs} selectedJobIds={selectedJobIds} />);
expect(wrapper.find('[data-test-subj="ml-warning-not-running-jobs"]')).toHaveLength(0);
});

it('shows warning if there are jobs not running', () => {
const wrapper = shallow(<HelpText href={'https://test.com'} notRunningJobIds={['id']} />);
const jobs = [
{
id: 'test-id',
jobState: 'closed',
datafeedState: 'stopped',
},
] as SecurityJob[];
const selectedJobIds = ['test-id'];

const wrapper = shallow(<HelpText jobs={jobs} selectedJobIds={selectedJobIds} />);
expect(wrapper.find('[data-test-subj="ml-warning-not-running-jobs"]')).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,84 @@
* 2.0.
*/

import React from 'react';
import React, { memo, useMemo } from 'react';
import { EuiLink, EuiText } from '@elastic/eui';
import styled from 'styled-components';
import { css } from '@emotion/css';

import { FormattedMessage } from '@kbn/i18n-react';
import { isJobStarted } from '../../../../../common/machine_learning/helpers';
import type { SecurityJob } from '../../../../common/components/ml_popover/types';
import { useKibana } from '../../../../common/lib/kibana';
interface HelpTextProps {
jobs: SecurityJob[];
selectedJobIds: string[];
}

const HelpTextWarningContainer = styled.div`
margin-top: 10px;
`;
export const HelpText = memo(function HelpText({
jobs,
selectedJobIds,
}: HelpTextProps): JSX.Element {
const { getUrlForApp } = useKibana().services.application;
const mlUrl = getUrlForApp('ml');

const HelpTextComponent: React.FC<{ href: string; notRunningJobIds: string[] }> = ({
href,
notRunningJobIds,
}) => (
<>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText"
defaultMessage="We've provided a few common jobs to get you started. To add your own custom jobs, assign a group of 'security' to those jobs in the {machineLearning} application to make them appear here."
values={{
machineLearning: (
<EuiLink href={href} target="_blank">
<FormattedMessage
id="xpack.securitySolution.components.mlJobSelect.machineLearningLink"
defaultMessage="Machine Learning"
/>
</EuiLink>
),
}}
/>
{notRunningJobIds.length > 0 && (
<HelpTextWarningContainer data-test-subj="ml-warning-not-running-jobs">
<EuiText size="xs">
<span>
{notRunningJobIds.length === 1 ? (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlEnableJobSingle"
defaultMessage="The selected ML job, {jobName}, is not currently running. We will start {jobName} when you enable this rule."
values={{
jobName: notRunningJobIds[0],
}}
/>
) : (
const notRunningJobIds = useMemo<string[]>(() => {
const selectedJobs = jobs.filter(({ id }) => selectedJobIds.includes(id));
return selectedJobs.reduce((acc, job) => {
if (!isJobStarted(job.jobState, job.datafeedState)) {
acc.push(job.id);
}
return acc;
}, [] as string[]);
}, [jobs, selectedJobIds]);

return (
<>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText"
defaultMessage="We've provided a few common jobs to get you started. To add your own custom jobs, assign a group of 'security' to those jobs in the {machineLearning} application to make them appear here."
values={{
machineLearning: (
<EuiLink href={mlUrl} target="_blank">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlEnableJobMulti"
defaultMessage="The selected ML jobs, {jobNames}, are not currently running. We will start all of these jobs when you enable this rule."
values={{
jobNames: notRunningJobIds.reduce(
(acc, value, i, array) => acc + (i < array.length - 1 ? ', ' : ', and ') + value
),
}}
id="xpack.securitySolution.components.mlJobSelect.machineLearningLink"
defaultMessage="Machine Learning"
/>
)}
</span>
</EuiText>
</HelpTextWarningContainer>
)}
</>
);
</EuiLink>
),
}}
/>
{notRunningJobIds.length > 0 && (
<div className={warningContainerClassName} data-test-subj="ml-warning-not-running-jobs">
<EuiText size="xs">
<span>
{notRunningJobIds.length === 1 ? (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlEnableJobSingle"
defaultMessage="The selected ML job, {jobName}, is not currently running. We will start {jobName} when you enable this rule."
values={{
jobName: notRunningJobIds[0],
}}
/>
) : (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlEnableJobMulti"
defaultMessage="The selected ML jobs, {jobNames}, are not currently running. We will start all of these jobs when you enable this rule."
values={{
jobNames: notRunningJobIds.reduce(
(acc, value, i, array) =>
acc + (i < array.length - 1 ? ', ' : ', and ') + value
),
}}
/>
)}
</span>
</EuiText>
</div>
)}
</>
);
});

export const HelpText = React.memo(HelpTextComponent);
const warningContainerClassName = css`
margin-top: 10px;
`;
Loading