Skip to content

Commit

Permalink
[SIEM] Add license check to ML Rule form (#60691) (#60928)
Browse files Browse the repository at this point in the history
* Gate ML Rules behind a license check

If they don't have a Platinum or Trial license, then we disable the ML
Card and provide them a link to the subscriptions marketing page.

* Add aria-describedby for new ML input fields

* Add data-test-subj to new ML input fields

* Remove unused prop

This is already passed as isLoading

* Fix capitalization on translation id

* Declare defaulted props as optional

* Gray out entire ML card when ML Rules are disabled

If we're editing an existing rule, or if the user has an insufficient
license, we disable both the card and its selectability. This is more
visually striking, and a more obvious CTA.
  • Loading branch information
rylnd authored Mar 23, 2020
1 parent d4e86e1 commit 3d14b3d
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ import { EuiFlexGrid, EuiFlexItem, EuiRange, EuiFormRow } from '@elastic/eui';
import { FieldHook } from '../../../../../shared_imports';

interface AnomalyThresholdSliderProps {
describedByIds: string[];
field: FieldHook;
}
type Event = React.ChangeEvent<HTMLInputElement>;
type EventArg = Event | React.MouseEvent<HTMLButtonElement>;

export const AnomalyThresholdSlider: React.FC<AnomalyThresholdSliderProps> = ({ field }) => {
export const AnomalyThresholdSlider: React.FC<AnomalyThresholdSliderProps> = ({
describedByIds = [],
field,
}) => {
const threshold = field.value as number;
const onThresholdChange = useCallback(
(event: EventArg) => {
Expand All @@ -26,7 +30,12 @@ export const AnomalyThresholdSlider: React.FC<AnomalyThresholdSliderProps> = ({
);

return (
<EuiFormRow label={field.label} fullWidth>
<EuiFormRow
fullWidth
label={field.label}
data-test-subj="anomalyThresholdSlider"
describedByIds={describedByIds}
>
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiRange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ const JobDisplay = ({ title, description }: { title: string; description: string
);

interface MlJobSelectProps {
describedByIds: string[];
field: FieldHook;
}

export const MlJobSelect: React.FC<MlJobSelectProps> = ({ field }) => {
export const MlJobSelect: React.FC<MlJobSelectProps> = ({ describedByIds = [], field }) => {
const jobId = field.value as string;
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
const [isLoading, siemJobs] = useSiemJobs(false);
Expand All @@ -41,7 +42,14 @@ export const MlJobSelect: React.FC<MlJobSelectProps> = ({ field }) => {
}));

return (
<EuiFormRow fullWidth label={field.label} isInvalid={isInvalid} error={errorMessage}>
<EuiFormRow
fullWidth
label={field.label}
isInvalid={isInvalid}
error={errorMessage}
data-test-subj="mlJobSelect"
describedByIds={describedByIds}
>
<EuiFlexGroup>
<EuiFlexItem>
<EuiSuperSelect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,58 @@
*/

import React, { useCallback } from 'react';
import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiIcon, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiCard,
EuiFlexGrid,
EuiFlexItem,
EuiFormRow,
EuiIcon,
EuiLink,
EuiText,
} from '@elastic/eui';

import { FieldHook } from '../../../../../shared_imports';
import { RuleType } from '../../../../../containers/detection_engine/rules/types';
import * as i18n from './translations';
import { isMlRule } from '../../helpers';

const MlCardDescription = ({ hasValidLicense = false }: { hasValidLicense?: boolean }) => (
<EuiText size="s">
{hasValidLicense ? (
i18n.ML_TYPE_DESCRIPTION
) : (
<FormattedMessage
id="xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription"
defaultMessage="Access to ML requires a {subscriptionsLink}."
values={{
subscriptionsLink: (
<EuiLink href="https://www.elastic.co/subscriptions" target="_blank">
<FormattedMessage
id="xpack.siem.components.stepDefineRule.ruleTypeField.subscriptionsLink"
defaultMessage="Platinum subscription"
/>
</EuiLink>
),
}}
/>
)}
</EuiText>
);

interface SelectRuleTypeProps {
describedByIds?: string[];
field: FieldHook;
isReadOnly: boolean;
hasValidLicense?: boolean;
isReadOnly?: boolean;
}

export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ field, isReadOnly = false }) => {
export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({
describedByIds = [],
field,
hasValidLicense = false,
isReadOnly = false,
}) => {
const ruleType = field.value as RuleType;
const setType = useCallback(
(type: RuleType) => {
Expand All @@ -27,10 +66,15 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ field, isReadOnl
);
const setMl = useCallback(() => setType('machine_learning'), [setType]);
const setQuery = useCallback(() => setType('query'), [setType]);
const license = true; // TODO
const mlCardDisabled = isReadOnly || !hasValidLicense;

return (
<EuiFormRow label={field.label} fullWidth>
<EuiFormRow
fullWidth
data-test-subj="selectRuleType"
describedByIds={describedByIds}
label={field.label}
>
<EuiFlexGrid columns={4}>
<EuiFlexItem>
<EuiCard
Expand All @@ -47,11 +91,11 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ field, isReadOnl
<EuiFlexItem>
<EuiCard
title={i18n.ML_TYPE_TITLE}
description={license ? i18n.ML_TYPE_DESCRIPTION : i18n.ML_TYPE_DISABLED_DESCRIPTION}
isDisabled={!license}
description={<MlCardDescription hasValidLicense={hasValidLicense} />}
icon={<EuiIcon size="l" type="machineLearningApp" />}
isDisabled={mlCardDisabled}
selectable={{
isDisabled: isReadOnly,
isDisabled: mlCardDisabled,
onClick: setMl,
isSelected: isMlRule(ruleType),
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,3 @@ export const ML_TYPE_DESCRIPTION = i18n.translate(
defaultMessage: 'Select ML job to detect anomalous activity.',
}
);

export const ML_TYPE_DISABLED_DESCRIPTION = i18n.translate(
'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription',
{
defaultMessage: 'Access to ML requires a Platinum subscription.',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import {
EuiButton,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
import React, { FC, memo, useCallback, useState, useEffect, useContext } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';

import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public';
import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules';
import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants';
import { MlCapabilitiesContext } from '../../../../../components/ml/permissions/ml_capabilities_provider';
import { useUiSetting$ } from '../../../../../lib/kibana';
import { setFieldValue, isMlRule } from '../../helpers';
import * as RuleI18n from '../../translations';
Expand Down Expand Up @@ -103,6 +104,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
setForm,
setStepData,
}) => {
const mlCapabilities = useContext(MlCapabilitiesContext);
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false);
const [localIsMlRule, setIsMlRule] = useState(false);
Expand Down Expand Up @@ -182,6 +184,8 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
path="ruleType"
component={SelectRuleType}
componentProps={{
describedByIds: ['detectionEngineStepDefineRuleType'],
hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense,
isReadOnly: isUpdateView,
}}
/>
Expand Down Expand Up @@ -220,7 +224,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
component={QueryBarDefineRule}
componentProps={{
browserFields,
loading: indexPatternLoadingQueryBar,
idAria: 'detectionEngineStepDefineRuleQueryBar',
indexPattern: indexPatternQueryBar,
isDisabled: isLoading,
Expand All @@ -234,8 +237,20 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
</EuiFormRow>
<EuiFormRow fullWidth style={{ display: localIsMlRule ? 'flex' : 'none' }}>
<>
<UseField path="machineLearningJobId" component={MlJobSelect} />
<UseField path="anomalyThreshold" component={AnomalyThresholdSlider} />
<UseField
path="machineLearningJobId"
component={MlJobSelect}
componentProps={{
describedByIds: ['detectionEngineStepDefineRulemachineLearningJobId'],
}}
/>
<UseField
path="anomalyThreshold"
component={AnomalyThresholdSlider}
componentProps={{
describedByIds: ['detectionEngineStepDefineRuleAnomalyThreshold'],
}}
/>
</>
</EuiFormRow>
<FormDataProvider pathsToWatch={['index', 'ruleType']}>
Expand Down

0 comments on commit 3d14b3d

Please sign in to comment.