[Security Solution] Setup field form component (#178131)

## Summary

Addresses https://github.com/elastic/kibana/issues/173626

Adds a markdown component in the create and edit rule forms so that
users are able to add their own setup guides to custom rules. Also
updates the `create` and `update` rule schemas and route logic to handle
these new cases through the API.

[Flaky test run
(internal)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5603)

### Screenshots
![Screenshot 2024-03-08 at 11 12
25 AM](5a00b007-d02d-4f1e-b1ba-ca7ba7f68bbd)


![Screenshot 2024-03-06 at 10 25
47 AM](a3973e10-1c82-4981-b38d-69faf06a5993)


### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Davis Plumlee 2024-04-08 07:55:29 -04:00 committed by GitHub
parent 4047cc862b
commit 9f8433e564
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 245 additions and 71 deletions

View file

@ -52,12 +52,12 @@ import {
RuleReferenceArray,
MaxSignals,
ThreatArray,
SetupGuide,
RuleObjectId,
RuleSignatureId,
IsRuleImmutable,
RelatedIntegrationArray,
RequiredFieldArray,
SetupGuide,
RuleQuery,
IndexPatternArray,
DataViewId,
@ -134,6 +134,7 @@ export const BaseDefaultableFields = z.object({
references: RuleReferenceArray.optional(),
max_signals: MaxSignals.optional(),
threat: ThreatArray.optional(),
setup: SetupGuide.optional(),
});
export type BaseCreateProps = z.infer<typeof BaseCreateProps>;
@ -162,7 +163,6 @@ export const ResponseFields = z.object({
revision: z.number().int().min(0),
related_integrations: RelatedIntegrationArray,
required_fields: RequiredFieldArray,
setup: SetupGuide,
execution_summary: RuleExecutionSummary.optional(),
});

View file

@ -128,6 +128,8 @@ components:
$ref: './common_attributes.schema.yaml#/components/schemas/MaxSignals'
threat:
$ref: './common_attributes.schema.yaml#/components/schemas/ThreatArray'
setup:
$ref: './common_attributes.schema.yaml#/components/schemas/SetupGuide'
BaseCreateProps:
x-inline: true
@ -174,7 +176,7 @@ components:
revision:
type: integer
minimum: 0
# NOTE: For now, Related Integrations, Required Fields and Setup Guide are
# NOTE: For now, Related Integrations and Required Fields are
# supported for prebuilt rules only. We don't want to allow users to edit these 3
# fields via the API. If we added them to baseParams.defaultable, they would
# become a part of the request schema as optional fields. This is why we add them
@ -183,8 +185,6 @@ components:
$ref: './common_attributes.schema.yaml#/components/schemas/RelatedIntegrationArray'
required_fields:
$ref: './common_attributes.schema.yaml#/components/schemas/RequiredFieldArray'
setup:
$ref: './common_attributes.schema.yaml#/components/schemas/SetupGuide'
execution_summary:
$ref: '../../rule_monitoring/model/execution_summary.schema.yaml#/components/schemas/RuleExecutionSummary'
required:
@ -198,7 +198,6 @@ components:
- revision
- related_integrations
- required_fields
- setup
SharedCreateProps:
x-inline: true
@ -279,7 +278,7 @@ components:
$ref: './specific_attributes/eql_attributes.schema.yaml#/components/schemas/TiebreakerField'
timestamp_field:
$ref: './specific_attributes/eql_attributes.schema.yaml#/components/schemas/TimestampField'
EqlRuleCreateFields:
allOf:
- $ref: '#/components/schemas/EqlRequiredFields'

View file

@ -31,6 +31,7 @@ interface MarkdownEditorProps {
height?: number;
autoFocusDisabled?: boolean;
setIsMarkdownInvalid: (value: boolean) => void;
includePlugins?: boolean;
}
type EuiMarkdownEditorRef = ElementRef<typeof EuiMarkdownEditor>;
@ -52,6 +53,7 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
height,
autoFocusDisabled,
setIsMarkdownInvalid,
includePlugins = true,
},
ref
) => {
@ -73,8 +75,8 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
const insightsUpsellingMessage = useUpsellingMessage('investigation_guide');
const uiPluginsWithState = useMemo(() => {
return uiPlugins({ insightsUpsellingMessage });
}, [insightsUpsellingMessage]);
return includePlugins ? uiPlugins({ insightsUpsellingMessage }) : undefined;
}, [insightsUpsellingMessage, includePlugins]);
// @ts-expect-error update types
useImperativeHandle(ref, () => {

View file

@ -23,6 +23,7 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & {
idAria: string;
isDisabled?: boolean;
bottomRightContent?: React.ReactNode;
includePlugins?: boolean;
};
/* eslint-enable react/no-unused-prop-types */
@ -34,7 +35,7 @@ const BottomContentWrapper = styled(EuiFlexGroup)`
export const MarkdownEditorForm = React.memo(
forwardRef<MarkdownEditorRef, MarkdownEditorFormProps>(
({ id, field, dataTestSubj, idAria, bottomRightContent }, ref) => {
({ id, field, dataTestSubj, idAria, bottomRightContent, includePlugins }, ref) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
const [isMarkdownInvalid, setIsMarkdownInvalid] = useState(false);
@ -58,6 +59,7 @@ export const MarkdownEditorForm = React.memo(
value={field.value as string}
data-test-subj={`${dataTestSubj}-markdown-editor`}
setIsMarkdownInvalid={setIsMarkdownInvalid}
includePlugins={includePlugins}
/>
{bottomRightContent && (
<BottomContentWrapper justifyContent={'flexEnd'}>

View file

@ -40,7 +40,7 @@ describe('StepAboutRuleToggleDetails', () => {
stepDataDetails={{
note: stepDataMock.note,
description: stepDataMock.description,
setup: '',
setup: stepDataMock.setup,
}}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
@ -82,28 +82,30 @@ describe('StepAboutRuleToggleDetails', () => {
});
describe('note value is empty string', () => {
test('it does not render toggle buttons', () => {
test('it does render toggle buttons if setup is not empty', () => {
const mockAboutStepWithoutNote = {
...stepDataMock,
note: '',
};
const wrapper = shallow(
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: '',
description: stepDataMock.description,
setup: '',
}}
stepData={mockAboutStepWithoutNote}
rule={mockRule('mocked-rule-id')}
/>
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: '',
description: stepDataMock.description,
setup: stepDataMock.setup,
}}
stepData={mockAboutStepWithoutNote}
rule={mockRule('mocked-rule-id')}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="stepAboutDetailsToggle"]').exists()).toBeFalsy();
expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy();
expect(wrapper.find('#details').at(0).prop('isSelected')).toBeTruthy();
expect(wrapper.find('#setup').at(0).prop('isSelected')).toBeFalsy();
expect(wrapper.find('[data-test-subj="stepAboutDetailsNoteContent"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="stepAboutDetailsSetupContent"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="stepAboutDetailsContent"]').exists()).toBeTruthy();
});
});
@ -116,7 +118,7 @@ describe('StepAboutRuleToggleDetails', () => {
stepDataDetails={{
note: stepDataMock.note,
description: stepDataMock.description,
setup: '',
setup: stepDataMock.setup,
}}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
@ -137,7 +139,7 @@ describe('StepAboutRuleToggleDetails', () => {
stepDataDetails={{
note: stepDataMock.note,
description: stepDataMock.description,
setup: '',
setup: stepDataMock.setup,
}}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
@ -212,7 +214,7 @@ describe('StepAboutRuleToggleDetails', () => {
stepDataDetails={{
note: stepDataMock.note,
description: stepDataMock.description,
setup: stepDataMock.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
setup: stepDataMock.setup,
}}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
@ -234,7 +236,7 @@ describe('StepAboutRuleToggleDetails', () => {
stepDataDetails={{
note: stepDataMock.note,
description: stepDataMock.description,
setup: stepDataMock.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
setup: stepDataMock.setup,
}}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
@ -253,7 +255,7 @@ describe('StepAboutRuleToggleDetails', () => {
expect(wrapper.find('[idSelected="setup"]').exists()).toBeTruthy();
});
test('it displays notes markdown when user toggles to "setup"', () => {
test('it displays setup markdown when user toggles to "setup"', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<StepAboutRuleToggleDetails
@ -261,7 +263,7 @@ describe('StepAboutRuleToggleDetails', () => {
stepDataDetails={{
note: stepDataMock.note,
description: stepDataMock.description,
setup: stepDataMock.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
setup: stepDataMock.setup,
}}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
@ -273,7 +275,7 @@ describe('StepAboutRuleToggleDetails', () => {
expect(wrapper.find('EuiButtonGroup[idSelected="setup"]').exists()).toBeTruthy();
expect(wrapper.find('div.euiMarkdownFormat').text()).toEqual(
'this is some markdown documentation'
'this is some setup documentation'
);
});
});

View file

@ -56,6 +56,11 @@ const NoteDescriptionContainer = styled(EuiFlexItem)`
overflow-y: hidden;
`;
const SetupDescriptionContainer = styled(EuiFlexItem)`
height: 105px;
overflow-y: hidden;
`;
export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join(''));
const EuiBadgeWrap = styled(EuiBadge)`
@ -647,3 +652,21 @@ export const buildAlertSuppressionMissingFieldsDescription = (
},
];
};
export const buildSetupDescription = (label: string, setup: string): ListItems[] => {
if (setup.trim() !== '') {
return [
{
title: label,
description: (
<SetupDescriptionContainer>
<div data-test-subj="setupDescriptionItem" className="eui-yScrollWithShadows">
{setup}
</div>
</SetupDescriptionContainer>
),
},
];
}
return [];
};

View file

@ -263,7 +263,7 @@ describe('description_step', () => {
mockLicenseService
);
expect(result.length).toEqual(12);
expect(result.length).toEqual(13);
});
});
@ -559,6 +559,21 @@ describe('description_step', () => {
});
});
describe('setup', () => {
test('returns default "setup" description', () => {
const result: ListItems[] = getDescriptionItem(
'setup',
'Setup guide',
mockAboutStep,
mockFilterManager,
mockLicenseService
);
expect(result[0].title).toEqual('Setup guide');
expect(React.isValidElement(result[0].description)).toBeTruthy();
});
});
describe('alert suppression', () => {
const ruleTypesWithoutSuppression: Type[] = ['eql', 'esql', 'machine_learning', 'new_terms'];
const suppressionFields = {

View file

@ -47,6 +47,7 @@ import {
buildAlertSuppressionWindowDescription,
buildAlertSuppressionMissingFieldsDescription,
buildHighlightedFieldsOverrideDescription,
buildSetupDescription,
getQueryLabel,
} from './helpers';
import * as i18n from './translations';
@ -305,6 +306,9 @@ export const getDescriptionItem = (
} else if (field === 'note') {
const val: string = get(field, data);
return buildNoteDescription(label, val);
} else if (field === 'setup') {
const val: string = get(field, data);
return buildSetupDescription(label, val);
} else if (field === 'ruleType') {
const ruleType: Type = get(field, data);
return buildRuleTypeDescription(label, ruleType);

View file

@ -33,4 +33,5 @@ export const stepAboutDefaultValue: AboutStepRule = {
timestampOverride: '',
threat: threatDefault,
note: '',
setup: '',
};

View file

@ -269,6 +269,7 @@ describe('StepAboutRuleComponent', () => {
falsePositives: [''],
name: 'Test name text',
note: '',
setup: '',
references: [''],
riskScore: { value: 21, mapping: [], isMappingChecked: false },
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
@ -329,6 +330,7 @@ describe('StepAboutRuleComponent', () => {
falsePositives: [''],
name: 'Test name text',
note: '',
setup: '',
references: [''],
riskScore: { value: 80, mapping: [], isMappingChecked: false },
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },

View file

@ -253,6 +253,18 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
}}
/>
<EuiSpacer size="l" />
<UseField
path="setup"
component={MarkdownEditorForm}
componentProps={{
idAria: 'detectionEngineStepAboutRuleSetup',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepAboutRuleSetup',
placeholder: I18n.ADD_RULE_SETUP_HELP_TEXT,
includePlugins: false,
}}
/>
<EuiSpacer size="l" />
<UseField
path="note"
component={MarkdownEditorForm}

View file

@ -305,6 +305,23 @@ export const schema: FormSchema<AboutStepRule> = {
),
labelAppend: OptionalFieldLabel,
},
setup: {
type: FIELD_TYPES.TEXTAREA,
label: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel',
{
defaultMessage: 'Setup guide',
}
),
helpText: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText',
{
defaultMessage:
'Provide instructions on rule prerequisites such as required integrations, configuration steps, and anything else needed for the rule to work correctly.',
}
),
labelAppend: OptionalFieldLabel,
},
};
const threatIndicatorPathRequiredSchemaValue = {

View file

@ -90,3 +90,10 @@ export const ADD_RULE_NOTE_HELP_TEXT = i18n.translate(
defaultMessage: 'Add rule investigation guide...',
}
);
export const ADD_RULE_SETUP_HELP_TEXT = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText',
{
defaultMessage: 'Add rule setup guide...',
}
);

View file

@ -556,6 +556,7 @@ describe('helpers', () => {
tags: ['tag1', 'tag2'],
threat: getThreatMock(),
investigation_fields: { field_names: ['foo', 'bar'] },
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);
@ -637,6 +638,7 @@ describe('helpers', () => {
tags: ['tag1', 'tag2'],
threat: getThreatMock(),
investigation_fields: { field_names: ['foo', 'bar'] },
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);
@ -662,6 +664,7 @@ describe('helpers', () => {
tags: ['tag1', 'tag2'],
threat: getThreatMock(),
investigation_fields: { field_names: ['foo', 'bar'] },
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);
@ -706,6 +709,7 @@ describe('helpers', () => {
tags: ['tag1', 'tag2'],
threat: getThreatMock(),
investigation_fields: { field_names: ['foo', 'bar'] },
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);
@ -759,6 +763,7 @@ describe('helpers', () => {
},
],
investigation_fields: { field_names: ['foo', 'bar'] },
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);
@ -788,6 +793,7 @@ describe('helpers', () => {
timestamp_override: 'event.ingest',
timestamp_override_fallback_disabled: true,
investigation_fields: { field_names: ['foo', 'bar'] },
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);
@ -818,6 +824,7 @@ describe('helpers', () => {
timestamp_override_fallback_disabled: undefined,
threat: getThreatMock(),
investigation_fields: undefined,
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);
@ -847,6 +854,7 @@ describe('helpers', () => {
threat_indicator_path: undefined,
timestamp_override: undefined,
timestamp_override_fallback_disabled: undefined,
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);
@ -876,6 +884,7 @@ describe('helpers', () => {
threat_indicator_path: undefined,
timestamp_override: undefined,
timestamp_override_fallback_disabled: undefined,
setup: '# this is some setup documentation',
};
expect(result).toEqual(expected);

View file

@ -81,7 +81,7 @@ export const mockRule = (id: string): SavedQueryRule => ({
meta: { from: '0m' },
related_integrations: [],
required_fields: [],
setup: '',
setup: '# this is some setup documentation',
severity: 'low',
severity_mapping: [],
updated_by: 'elastic',
@ -149,7 +149,7 @@ export const mockRuleWithEverything = (id: string): RuleResponse => ({
meta: { from: '0m' },
related_integrations: [],
required_fields: [],
setup: '',
setup: '# this is some setup documentation',
severity: 'low',
severity_mapping: [],
updated_by: 'elastic',
@ -197,6 +197,7 @@ export const mockAboutStepRule = (): AboutStepRule => ({
tags: ['tag1', 'tag2'],
threat: getThreatMock(),
note: '# this is some markdown documentation',
setup: '# this is some setup documentation',
investigationFields: ['foo', 'bar'],
});

View file

@ -146,6 +146,7 @@ describe('rule helpers', () => {
timestampOverride: 'event.ingested',
timestampOverrideFallbackDisabled: false,
investigationFields: [],
setup: '# this is some setup documentation',
};
const scheduleRuleStepData = { from: '0s', interval: '5m' };
const ruleActionsStepData = {
@ -156,7 +157,7 @@ describe('rule helpers', () => {
const aboutRuleDataDetailsData = {
note: '# this is some markdown documentation',
description: '24/7',
setup: '',
setup: '# this is some setup documentation',
};
expect(defineRuleData).toEqual(defineRuleStepData);
@ -195,18 +196,18 @@ describe('rule helpers', () => {
describe('determineDetailsValue', () => {
test('returns name, description, and note as empty string if detailsView is true', () => {
const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue(
const result: Pick<Rule, 'name' | 'description' | 'note' | 'setup'> = determineDetailsValue(
mockRuleWithEverything('test-id'),
true
);
const expected = { name: '', description: '', note: '' };
const expected = { name: '', description: '', note: '', setup: '' };
expect(result).toEqual(expected);
});
test('returns name, description, and note values if detailsView is false', () => {
const mockedRule = mockRuleWithEverything('test-id');
const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue(
const result: Pick<Rule, 'name' | 'description' | 'note' | 'setup'> = determineDetailsValue(
mockedRule,
false
);
@ -214,6 +215,7 @@ describe('rule helpers', () => {
name: mockedRule.name,
description: mockedRule.description,
note: mockedRule.note,
setup: mockedRule.setup,
};
expect(result).toEqual(expected);
@ -222,11 +224,16 @@ describe('rule helpers', () => {
test('returns note as empty string if property does not exist on rule', () => {
const mockedRule = mockRuleWithEverything('test-id');
delete mockedRule.note;
const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue(
const result: Pick<Rule, 'name' | 'description' | 'note' | 'setup'> = determineDetailsValue(
mockedRule,
false
);
const expected = { name: mockedRule.name, description: mockedRule.description, note: '' };
const expected = {
name: mockedRule.name,
description: mockedRule.description,
note: '',
setup: mockedRule.setup,
};
expect(result).toEqual(expected);
});
@ -418,7 +425,7 @@ describe('rule helpers', () => {
const aboutRuleDataDetailsData = {
note: '# this is some markdown documentation',
description: '24/7',
setup: '',
setup: '# this is some setup documentation',
};
expect(result).toEqual(aboutRuleDataDetailsData);
@ -431,7 +438,7 @@ describe('rule helpers', () => {
const aboutRuleDetailsData = {
note: '',
description: mockRuleWithoutNote.description,
setup: '',
setup: '# this is some setup documentation',
};
expect(result).toEqual(aboutRuleDetailsData);

View file

@ -222,7 +222,7 @@ export const getHumanizedDuration = (from: string, interval: string): string =>
};
export const getAboutStepsData = (rule: RuleResponse, detailsView: boolean): AboutStepRule => {
const { name, description, note } = determineDetailsValue(rule, detailsView);
const { name, description, note, setup } = determineDetailsValue(rule, detailsView);
const {
author,
building_block_type: buildingBlockType,
@ -272,6 +272,7 @@ export const getAboutStepsData = (rule: RuleResponse, detailsView: boolean): Abo
investigationFields: investigationFields?.field_names ?? [],
threat: threat as Threats,
threatIndicatorPath,
setup,
};
};
@ -296,13 +297,13 @@ export const fillEmptySeverityMappings = (mappings: SeverityMapping): SeverityMa
export const determineDetailsValue = (
rule: RuleResponse,
detailsView: boolean
): Pick<RuleResponse, 'name' | 'description' | 'note'> => {
const { name, description, note } = rule;
): Pick<RuleResponse, 'name' | 'description' | 'note' | 'setup'> => {
const { name, description, note, setup } = rule;
if (detailsView) {
return { name: '', description: '', note: '' };
return { name: '', description: '', note: '', setup: '' };
}
return { name, description, note: note ?? '' };
return { name, description, setup, note: note ?? '' };
};
export const getModifiedAboutDetailsData = (rule: RuleResponse): AboutStepRuleDetails => ({

View file

@ -101,6 +101,7 @@ export interface AboutStepRule {
threatIndicatorPath?: string;
threat: Threats;
note: string;
setup: SetupGuide;
}
export interface AboutStepRuleDetails {
@ -240,6 +241,7 @@ export interface AboutStepRuleJson {
rule_name_override?: RuleNameOverride;
tags: string[];
threat: Threats;
setup: string;
threat_indicator_path?: string;
timestamp_override?: TimestampOverride;
timestamp_override_fallback_disabled?: boolean;

View file

@ -93,6 +93,7 @@ export const stepAboutDefaultValue: AboutStepRule = {
timestampOverride: '',
threat: threatDefault,
note: '',
setup: '',
threatIndicatorPath: undefined,
timestampOverrideFallbackDisabled: undefined,
};

View file

@ -224,22 +224,6 @@ describe('duplicateRule', () => {
})
);
});
it('resets setup guide to an empty string', async () => {
const rule = createPrebuiltRule();
rule.params.setup = `## Config\n\nThe 'Audit Detailed File Share' audit policy must be configured...`;
const result = await duplicateRule({
rule,
});
expect(result).toEqual(
expect.objectContaining({
params: expect.objectContaining({
setup: '',
}),
})
);
});
});
describe('when duplicating a custom (mutable) rule', () => {

View file

@ -33,7 +33,6 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise<Inte
const isPrebuilt = rule.params.immutable;
const relatedIntegrations = isPrebuilt ? [] : rule.params.relatedIntegrations;
const requiredFields = isPrebuilt ? [] : rule.params.requiredFields;
const setup = isPrebuilt ? '' : rule.params.setup;
const actions = transformToActionFrequency(rule.actions, rule.throttle);
return {
@ -47,7 +46,6 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise<Inte
ruleId,
relatedIntegrations,
requiredFields,
setup,
exceptionsList: [],
},
schedule: rule.schedule,

View file

@ -62,7 +62,7 @@ export const updateRules = async ({
riskScore: ruleUpdate.risk_score,
riskScoreMapping: ruleUpdate.risk_score_mapping ?? [],
ruleNameOverride: ruleUpdate.rule_name_override,
setup: existingRule.params.setup,
setup: ruleUpdate.setup,
severity: ruleUpdate.severity,
severityMapping: ruleUpdate.severity_mapping ?? [],
threat: ruleUpdate.threat ?? [],

View file

@ -23,7 +23,6 @@ import type { PatchRuleRequestBody } from '../../../../../common/api/detection_e
import type {
RelatedIntegrationArray,
RequiredFieldArray,
SetupGuide,
RuleCreateProps,
TypeSpecificCreateProps,
TypeSpecificResponse,
@ -426,7 +425,6 @@ export const convertPatchAPIToInternalSchema = (
nextParams: PatchRuleRequestBody & {
related_integrations?: RelatedIntegrationArray;
required_fields?: RequiredFieldArray;
setup?: SetupGuide;
},
existingRule: SanitizedRule<RuleParams>
): InternalRuleUpdate => {
@ -487,7 +485,6 @@ export const convertCreateAPIToInternalSchema = (
input: RuleCreateProps & {
related_integrations?: RelatedIntegrationArray;
required_fields?: RequiredFieldArray;
setup?: SetupGuide;
},
immutable = false,
defaultEnabled = true

View file

@ -691,5 +691,27 @@ export default ({ getService }: FtrProviderContext) => {
});
});
});
describe('setup guide', async () => {
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('creates a rule with a setup guide when setup parameter is present', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(
getCustomQueryRuleParams({
setup: 'A setup guide',
})
)
.expect(200);
expect(body.setup).toEqual('A setup guide');
});
});
});
};

View file

@ -656,5 +656,37 @@ export default ({ getService }: FtrProviderContext) => {
});
});
});
describe('setup guide', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should overwrite setup field on patch', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
setup: 'A setup guide',
});
const rulePatch = {
rule_id: 'rule-1',
setup: 'A different setup guide',
};
const { body } = await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(rulePatch)
.expect(200);
expect(body.setup).to.eql('A different setup guide');
});
});
});
};

View file

@ -757,6 +757,40 @@ export default ({ getService }: FtrProviderContext) => {
expect(body.investigation_fields).to.eql(undefined);
});
});
describe('setup guide', () => {
it('should overwrite setup value on update', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
setup: 'A setup guide',
});
const ruleUpdate = {
...getSimpleRuleUpdate('rule-1'),
setup: 'A different setup guide',
};
const { body } = await securitySolutionApi.updateRule({ body: ruleUpdate }).expect(200);
expect(body.setup).to.eql('A different setup guide');
});
it('should reset setup field to empty string on unset', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
setup: 'A setup guide',
});
const ruleUpdate = {
...getSimpleRuleUpdate('rule-1'),
setup: undefined,
};
const { body } = await securitySolutionApi.updateRule({ body: ruleUpdate }).expect(200);
expect(body.setup).to.eql('');
});
});
});
});
};