diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts index 9d27297d11bbe..d7a8b83ec28f4 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts @@ -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; @@ -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(), }); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml index 22308557c3aaa..d3a09d8355727 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml @@ -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' diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx index 64d289cd65f3e..2f439c55a7d1c 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx @@ -31,6 +31,7 @@ interface MarkdownEditorProps { height?: number; autoFocusDisabled?: boolean; setIsMarkdownInvalid: (value: boolean) => void; + includePlugins?: boolean; } type EuiMarkdownEditorRef = ElementRef; @@ -52,6 +53,7 @@ const MarkdownEditorComponent = forwardRef { @@ -73,8 +75,8 @@ const MarkdownEditorComponent = forwardRef { - return uiPlugins({ insightsUpsellingMessage }); - }, [insightsUpsellingMessage]); + return includePlugins ? uiPlugins({ insightsUpsellingMessage }) : undefined; + }, [insightsUpsellingMessage, includePlugins]); // @ts-expect-error update types useImperativeHandle(ref, () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx index dc157a85afa2b..8fdbc3559bbc4 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx @@ -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( - ({ 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 && ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.test.tsx index 953e75ab5ceda..ec39abb61465a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.test.tsx @@ -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( - + const wrapper = mount( + + + ); - 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( { 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' ); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/helpers.tsx index 446ff21d9414d..222920c536917 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/helpers.tsx @@ -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: ( + +
+ {setup} +
+
+ ), + }, + ]; + } + return []; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx index c4770a1640704..f341476c4d8f9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx @@ -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 = { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx index 9f377755c769e..78bcd60e5c0d6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx @@ -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); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/default_value.ts index 91057eb3ff5f8..26f842384ef25 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/default_value.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/default_value.ts @@ -33,4 +33,5 @@ export const stepAboutDefaultValue: AboutStepRule = { timestampOverride: '', threat: threatDefault, note: '', + setup: '', }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx index d654eaef9cca7..dc3fc5645b138 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx @@ -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 }, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx index 839618669dc06..99e65f33e486a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx @@ -253,6 +253,18 @@ const StepAboutRuleComponent: FC = ({ }} /> + + = { ), 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 = { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/translations.ts index 007cf4d9dd4c6..d07fe22a8ed7b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/translations.ts @@ -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...', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts index 86bcebb72ded5..71fe20ba3e6fb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts @@ -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); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts index 4c23c14871067..49bd1649c3471 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts @@ -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'], }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 2ffedcdc55568..bcb73b1f9edc2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -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 = determineDetailsValue( + const result: Pick = 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 = determineDetailsValue( + const result: Pick = 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 = determineDetailsValue( + const result: Pick = 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); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 96a3b17a77871..574397c80e767 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -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 => { - const { name, description, note } = rule; +): Pick => { + 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 => ({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index f57184a3a490b..fa0168c7d2e98 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -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; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index 9e54856b7b28c..565180217f842 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -93,6 +93,7 @@ export const stepAboutDefaultValue: AboutStepRule = { timestampOverride: '', threat: threatDefault, note: '', + setup: '', threatIndicatorPath: undefined, timestampOverrideFallbackDisabled: undefined, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts index 7223b920c7bdc..c0cb5f903c3ea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts @@ -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', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts index 315517504def4..57931dca00c1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts @@ -33,7 +33,6 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise ): InternalRuleUpdate => { @@ -487,7 +485,6 @@ export const convertCreateAPIToInternalSchema = ( input: RuleCreateProps & { related_integrations?: RelatedIntegrationArray; required_fields?: RequiredFieldArray; - setup?: SetupGuide; }, immutable = false, defaultEnabled = true diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/create_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/create_rules.ts index 19b3188a4b8ad..e7967df45b5f9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/create_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/create_rules.ts @@ -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'); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules.ts index edd84f6c86650..24919448b8522 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules.ts @@ -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'); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/update_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/update_rules.ts index fe59a0127bb82..500eedb5bc2fd 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/update_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/update_rules.ts @@ -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(''); + }); + }); }); }); };