Skip to content

Commit c7aa763

Browse files
authored
[Security Solution][Detections] Rule forms cleanup (#76138) (#76827)
* Remove unused isNew field Digging through the git history, it looks like this was replaced with the isUpdateView prop at some point. There's a small chance that we're indirectly leveraging the effect of this value being changed, but I think we're safe. * WIP: Making rule form type safe We have lots of anys and unknowns in here, this is my attempt to fix that. I started by defining better types for the state/refs in our parent component; everything else mostly flowed out of that: * Step components now type their form hook for their step's data * Removes lots of unneeded `as` casts * Renames uses of `accordionId` with `step` and `activeStep`, since they are also values of our `RuleStep` enum * Step components now export their default values * The data flow is simpler when the parent passes these values in, rather than trying to merge props with some internal defaults. * The internal defaulting is still there, but I think it'll be unnecessary once I audit the `edit` forms. I've only done this work for the Define step for now, the rest are next. * Make defaultValues a required prop of the define step Now that the create step is passing in the default values, we no longer need to merge with internal state. The one exception is the default indexes; since we need that data for our "reset to default indexes" behavior, we'll keep that functionality within our DefineStep component. * Refactor rule creation forms to not require default values We don't gain much by forcing the parent to pass in default values. The slightly cleaner types are not worth the burden to the parent; instead, we add a type guard to be used in our parent to ensure our values are present before working with them. Previously, we were circumventing this logic with an `as` cast. * Remove unnecessary "deep" comparison These are arrays of strings, so a shallow comparison should suffice here. Also reorders conditions to short-circuit on simple booleans first. * Make StepRuleDescription generic on its schema * Fixes bug introduced by form lib updates There's currently a bug on master where returning to a previous form step does not populate its previous values. After some investigation it appears that this is due to form values being reset on submission (form.reset()). Previously, we kept a separate copy of data in each step's state, and had a useEffect that would repopulate the form's values if they ever became out of sync. Once that was removed I believe this bug was introduced. For now the fix is effectively reimplementing the above behavior, albeit a little more elegantly with `reset()`. If we can restructure the form logic to only require the form data at the end (big if), then we can remove the need to "repopulate" these values to the form. For now, this does remove the local copy of data in the step component as I believe that is no longer needed. Data is stored in the parent, copied/modified to the form, and pushed back up when one clicks on to the next step. * Rename typed hook to obviate eslint exception The linter was complaining because it didn't think that `typedUseKibana` was a hook. But it is, and we should name it as such. * WIP: Fixing type errors in the other form steps Things still aren't quite working, state gets lost when moving through steps but I believe this is addressed in an outstanding PR so I'm not sweating it right now. * Removes as much state in Step components as possible * We shouldn't need this as the form holds all the state as well. If we need to "watch" for a change, we can subscribe to the form's observable to replace FormDataProvider and local state (TODO) * Removes setting of default values in form components * I believe that this is redundant with defaultValues provided to useForm, but I need to verify. * More form cleanup * Removes redundant uses of field's defaultValue * Most are redundant with the form's defaultValue; added calls to field.setValue in cases where the default is generated internally * Removes calls to reset() after submitting * These were needed due to a bug in the form lib; once #75796 is merged these will no longer be necessary. * Fix some leftover type errors * Remove duplicated useEffect hook This exists identically earlier in the component; I'm guessing it was the result of a bad merge conflict. * Fix Rule edit form * Makes data structures more similar to rule creation form * Adds type guards for asserting which step is "active" * Simplifies logic around the active tabe/step/form * Fixes About Step jest tests * Removes use of wait() in favor of act() * Fixes mock call assertion now that we're no longer setting our data to null (which was a now-unnecessary form lib workaround). * Fix bug with going to a previous step after editing actions We never send our actions data back down to the actions form, so it was lost if you went to a previous step. Since the actions UI still had any connectors you created, you merely had to reselect the throttle and the connector, but this prevents you from having to do that. * Add assertions to our rule creation test Asserts that our rule form repopulates with the provided values when going back to a previous step. This is to cover a regression that was not caught by CI (but which has now been fixed). * Simplify Rule Creation logic * Validation and data collection are performed in the parent, not the step component * Step component provides a form ref and notifies the parent when it's being submitted; the rest is the parent's responsibility * Renames some internal helper functions to be more declarative: submitStep, editStep, etc. * Don't persist empty form data when leaving a form step If the active step form is invalid we will receive no data, so we must not persist that lest the form blows up on absent values when we later navigate back. * Skip About Step tests for now These exercise functionality that was moved into the parent, so they need a new home. * Remove unnecessary calls to setValue * Instead of setting our kibana url after the form is created, we add it to the form's default state * We do not need to set the throttle field value, the field component already does this * Style: logic cleanup * Prevent users from navigating away from an invalid step on rule edit We can go against the form lib conventions and persist this invalid data ourselves on transition, but for now this brings the create/edit forms into alignment so that adding the aforementioned behavior should be nearly identical on both. * Display callout if attempting to navigate away from an invalid tab We already do this if you try to _submit_ the form, but not when you click on another tab. * Persist our form submit() rather than the entire form Making the entire stateful form a useEffect dependency was likely causing unnecessary render cycles. This may also have been part of why both the hooks and the data are refs instead of normal state; to prevent these rerenders. * Replace FormDataProvider with useFormData hook We have to do a type cast here because the hook's data is not typed, but other than that this is a solid win and cleans things up immensely; the side effects that result from these values changing are much more apparent (IMO). * Move fetch of fields data _after_ form initialization This ensures that our first fetch of fields will use the index patterns on the form, not necessarily the default ones. * Replace FormDataProvider on About step * This fixes a bug where changing the default severity no longer updated the default risk score. It looks like this was broken when the severity/riskScore overrides were added, and the values of these fields changed from primitives to objects. * Replace local state with useFormData By watching the value directly from the form we no longer have a need for local state, as we were just using it to determine whether the throttle had changed from the default. * Types cleanup Remove some unneeded casts, add some needed ones. * Rewrite About Step tests Rather than asserting that the form is invalid through the UI, we instead retrieve data out of the form hook and assert on that instead. * Add memoization back to StepRuleDescription I'm not sure that it's necessary, but best to leave it until we have time to audit/remove multiple of these. * Do not fetch ML Jobs if StepRuleDescription is not rendering ML Data We were incorrectly invoking the useSecurityJobs hook any time the StepRuleDescription component was rendered, regardless of whether we actually needed that data. This moves the useSecurityJobs hook into the component that uses it, MlJobDescription. If we end up having multiple of these on the page we can evaluate caching/sharing this data somehow, but for now this prevents: * 3x3=9 unnecessary ML calls on the Rule Create page. * 1x3=3 unnecessary ML calls on Rule Details * 1x3=3 unnecessary ML Calls on the Rule Edit page. * Fix bug where revisiting the About step could modify the user's Risk Score With the severity/risk score link back in place, there was a bug where a user who had previously set a manual risk score would have it rewritten on edit (or edit during rule creation). This was due to a poorly-written useEffect that basically said "if there is a severity, set a risk score." This has now been amended to say "if the user changes the severity, set a risk score." * Clean up About Step tests * We don't need act(), it's not doing anything. * We don't need to click Continue since we're just talking to the form hook * Fix local form data when form isn't mounted If the form isn't on the page (e.g. if we're read-only), then useFormData will return no values. In these cases, we can simply fall back to the initialState values, as they'll either be: the default values on a new form, or: the current values on an active create/edit form. Updates the manual type of useFormData to reflect this "maybe" fact. * Allow user to navigate between invalid tabs on Edit Rule * Form hooks now _always_ return the form's data, regardless of validity * Edit Rule now: * persists invalid data * submits both the active form and the destination form on navigation. This is necessary to refresh validations on the destination form, since the form lib assumes a newly-mounted form is valid * simplifies "invalid tab" logic to be derived from our persisted data * Fix logical error If the rule is immutable, they can only edit actions. * Remove unneeded eslint exception Fixed by upstream #76471 * Make 21 the default risk score for a new rule Since the default severity is 'low,' these two defaults now coincide. * Remove duplicated type in favor of common one
1 parent 0bbfd00 commit c7aa763

File tree

29 files changed

+686
-709
lines changed

29 files changed

+686
-709
lines changed

x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import {
5858
createAndActivateRule,
5959
fillAboutRuleAndContinue,
6060
fillDefineCustomRuleWithImportedQueryAndContinue,
61+
expectDefineFormToRepopulateAndContinue,
62+
expectAboutFormToRepopulateAndContinue,
6163
} from '../tasks/create_new_rule';
6264
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
6365
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
@@ -82,6 +84,8 @@ describe('Detection rules, custom', () => {
8284
goToCreateNewRule();
8385
fillDefineCustomRuleWithImportedQueryAndContinue(newRule);
8486
fillAboutRuleAndContinue(newRule);
87+
expectDefineFormToRepopulateAndContinue(newRule);
88+
expectAboutFormToRepopulateAndContinue(newRule);
8589
createAndActivateRule();
8690

8791
cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');

x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
export const ABOUT_CONTINUE_BTN = '[data-test-subj="about-continue"]';
88

9+
export const ABOUT_EDIT_BUTTON = '[data-test-subj="edit-about-rule"]';
10+
911
export const ADD_FALSE_POSITIVE_BTN =
1012
'[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text';
1113

@@ -26,6 +28,8 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]';
2628

2729
export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]';
2830

31+
export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]';
32+
2933
export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK =
3034
'[data-test-subj="importQueryFromSavedTimeline"]';
3135

x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import {
4848
THRESHOLD_FIELD_SELECTION,
4949
THRESHOLD_INPUT_AREA,
5050
THRESHOLD_TYPE,
51+
DEFINE_EDIT_BUTTON,
52+
ABOUT_EDIT_BUTTON,
5153
} from '../screens/create_new_rule';
5254
import { TIMELINE } from '../screens/timeline';
5355

@@ -175,6 +177,20 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = (
175177
cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
176178
};
177179

180+
export const expectDefineFormToRepopulateAndContinue = (rule: CustomRule) => {
181+
cy.get(DEFINE_EDIT_BUTTON).click();
182+
cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery);
183+
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
184+
cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist');
185+
};
186+
187+
export const expectAboutFormToRepopulateAndContinue = (rule: CustomRule) => {
188+
cy.get(ABOUT_EDIT_BUTTON).click();
189+
cy.get(RULE_NAME_INPUT).invoke('val').should('eq', rule.name);
190+
cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true });
191+
cy.get(ABOUT_CONTINUE_BTN).should('not.exist');
192+
};
193+
178194
export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => {
179195
const thresholdField = 0;
180196
const threshold = 1;

x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ export interface WithKibanaProps {
1919
kibana: KibanaContext;
2020
}
2121

22-
// eslint-disable-next-line react-hooks/rules-of-hooks
23-
const typedUseKibana = () => useKibana<StartServices>();
22+
const useTypedKibana = () => useKibana<StartServices>();
2423

2524
export {
2625
KibanaContextProvider,
27-
typedUseKibana as useKibana,
26+
useTypedKibana as useKibana,
2827
useUiSetting,
2928
useUiSetting$,
3029
withKibana,

x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React from 'react';
77
import { shallow, mount } from 'enzyme';
88

99
import {
10-
StepRuleDescriptionComponent,
10+
StepRuleDescription,
1111
addFilterStateIfNotThere,
1212
buildListItems,
1313
getDescriptionItem,
@@ -52,24 +52,24 @@ describe('description_step', () => {
5252
mockAboutStep = mockAboutStepRule();
5353
});
5454

55-
describe('StepRuleDescriptionComponent', () => {
55+
describe('StepRuleDescription', () => {
5656
test('renders tow columns when "columns" is "multi"', () => {
5757
const wrapper = shallow(
58-
<StepRuleDescriptionComponent columns="multi" data={mockAboutStep} schema={schema} />
58+
<StepRuleDescription columns="multi" data={mockAboutStep} schema={schema} />
5959
);
6060
expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2);
6161
});
6262

6363
test('renders single column when "columns" is "single"', () => {
6464
const wrapper = shallow(
65-
<StepRuleDescriptionComponent columns="single" data={mockAboutStep} schema={schema} />
65+
<StepRuleDescription columns="single" data={mockAboutStep} schema={schema} />
6666
);
6767
expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1);
6868
});
6969

7070
test('renders one column with title and description split when "columns" is "singleSplit', () => {
7171
const wrapper = shallow(
72-
<StepRuleDescriptionComponent columns="singleSplit" data={mockAboutStep} schema={schema} />
72+
<StepRuleDescription columns="singleSplit" data={mockAboutStep} schema={schema} />
7373
);
7474
expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1);
7575
expect(
@@ -299,7 +299,6 @@ describe('description_step', () => {
299299
describe('queryBar', () => {
300300
test('returns array of ListItems when queryBar exist', () => {
301301
const mockQueryBar = {
302-
isNew: false,
303302
queryBar: {
304303
query: {
305304
query: 'user.name: root or user.name: admin',
@@ -369,7 +368,6 @@ describe('description_step', () => {
369368
describe('threshold', () => {
370369
test('returns threshold description when threshold exist and field is empty', () => {
371370
const mockThreshold = {
372-
isNew: false,
373371
threshold: {
374372
field: [''],
375373
value: 100,
@@ -391,7 +389,6 @@ describe('description_step', () => {
391389

392390
test('returns threshold description when threshold exist and field is set', () => {
393391
const mockThreshold = {
394-
isNew: false,
395392
threshold: {
396393
field: ['user.name'],
397394
value: 100,

x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import {
3737
buildRuleTypeDescription,
3838
buildThresholdDescription,
3939
} from './helpers';
40-
import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs';
4140
import { buildMlJobDescription } from './ml_job_description';
4241
import { buildActionsDescription } from './actions_description';
4342
import { buildThrottleDescription } from './throttle_description';
@@ -52,22 +51,21 @@ const DescriptionListContainer = styled(EuiDescriptionList)`
5251
}
5352
`;
5453

55-
interface StepRuleDescriptionProps {
54+
interface StepRuleDescriptionProps<T> {
5655
columns?: 'multi' | 'single' | 'singleSplit';
5756
data: unknown;
5857
indexPatterns?: IIndexPattern;
59-
schema: FormSchema;
58+
schema: FormSchema<T>;
6059
}
6160

62-
export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({
61+
export const StepRuleDescriptionComponent = <T,>({
6362
data,
6463
columns = 'multi',
6564
indexPatterns,
6665
schema,
67-
}) => {
66+
}: StepRuleDescriptionProps<T>) => {
6867
const kibana = useKibana();
6968
const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings));
70-
const { jobs } = useSecurityJobs(false);
7169

7270
const keys = Object.keys(schema);
7371
const listItems = keys.reduce((acc: ListItems[], key: string) => {
@@ -76,8 +74,7 @@ export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> =
7674
...acc,
7775
buildMlJobDescription(
7876
get(key, data) as string,
79-
(get(key, schema) as { label: string }).label,
80-
jobs
77+
(get(key, schema) as { label: string }).label
8178
),
8279
];
8380
}
@@ -125,11 +122,13 @@ export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> =
125122
);
126123
};
127124

128-
export const StepRuleDescription = memo(StepRuleDescriptionComponent);
125+
export const StepRuleDescription = memo(
126+
StepRuleDescriptionComponent
127+
) as typeof StepRuleDescriptionComponent;
129128

130-
export const buildListItems = (
129+
export const buildListItems = <T,>(
131130
data: unknown,
132-
schema: FormSchema,
131+
schema: FormSchema<T>,
133132
filterManager: FilterManager,
134133
indexPatterns?: IIndexPattern
135134
): ListItems[] =>

x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jest.mock('../../../../common/lib/kibana');
1414

1515
describe('MlJobDescription', () => {
1616
it('renders correctly', () => {
17-
const wrapper = shallow(<MlJobDescription job={mockOpenedJob} />);
17+
const wrapper = shallow(<MlJobDescription jobId={'myJobId'} />);
1818

1919
expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1);
2020
});

x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
1010

1111
import { MlSummaryJob } from '../../../../../../ml/public';
1212
import { isJobStarted } from '../../../../../common/machine_learning/helpers';
13+
import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs';
1314
import { useKibana } from '../../../../common/lib/kibana';
1415
import { ListItems } from './types';
1516
import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations';
@@ -69,35 +70,33 @@ const Wrapper = styled.div`
6970
overflow: hidden;
7071
`;
7172

72-
const MlJobDescriptionComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => {
73+
const MlJobDescriptionComponent: React.FC<{ jobId: string }> = ({ jobId }) => {
74+
const { jobs } = useSecurityJobs(false);
7375
const jobUrl = useKibana().services.application.getUrlForApp(
74-
`ml#/jobs?mlManagement=(jobId:${encodeURI(job.id)})`
76+
`ml#/jobs?mlManagement=(jobId:${encodeURI(jobId)})`
7577
);
78+
const job = jobs.find(({ id }) => id === jobId);
7679

77-
return (
80+
const jobIdSpan = <span data-test-subj="machineLearningJobId">{jobId}</span>;
81+
82+
return job != null ? (
7883
<Wrapper>
7984
<div>
80-
<JobLink data-test-subj="machineLearningJobId" href={jobUrl} target="_blank">
81-
{job.id}
85+
<JobLink href={jobUrl} target="_blank">
86+
{jobIdSpan}
8287
</JobLink>
8388
<AuditIcon message={job.auditMessage} />
8489
</div>
8590
<JobStatusBadge job={job} />
8691
</Wrapper>
92+
) : (
93+
jobIdSpan
8794
);
8895
};
8996

9097
export const MlJobDescription = React.memo(MlJobDescriptionComponent);
9198

92-
export const buildMlJobDescription = (
93-
jobId: string,
94-
label: string,
95-
jobs: MlSummaryJob[]
96-
): ListItems => {
97-
const job = jobs.find(({ id }) => id === jobId);
98-
99-
return {
100-
title: label,
101-
description: job ? <MlJobDescription job={job} /> : jobId,
102-
};
103-
};
99+
export const buildMlJobDescription = (jobId: string, label: string): ListItems => ({
100+
title: label,
101+
description: <MlJobDescription jobId={jobId} />,
102+
});

x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elasti
99
import * as RuleI18n from '../../../pages/detection_engine/rules/translations';
1010

1111
interface NextStepProps {
12-
onClick: () => Promise<void>;
12+
onClick: () => void;
1313
isDisabled: boolean;
1414
dataTestSubj?: string;
1515
}

x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import styled from 'styled-components';
88
import { EuiHealth } from '@elastic/eui';
99
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
1010
import React from 'react';
11-
import * as I18n from './translations';
1211

13-
export type SeverityValue = 'low' | 'medium' | 'high' | 'critical';
12+
import { Severity } from '../../../../../common/detection_engine/schemas/common/schemas';
13+
import * as I18n from './translations';
1414

1515
export interface SeverityOptionItem {
16-
value: SeverityValue;
16+
value: Severity;
1717
inputDisplay: React.ReactElement;
1818
}
1919

@@ -44,7 +44,7 @@ export const severityOptions: SeverityOptionItem[] = [
4444
},
4545
];
4646

47-
export const defaultRiskScoreBySeverity: Record<SeverityValue, number> = {
47+
export const defaultRiskScoreBySeverity: Record<Severity, number> = {
4848
low: 21,
4949
medium: 47,
5050
high: 73,

0 commit comments

Comments
 (0)