mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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   ### 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:
parent
4047cc862b
commit
9f8433e564
26 changed files with 245 additions and 71 deletions
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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, () => {
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 [];
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -33,4 +33,5 @@ export const stepAboutDefaultValue: AboutStepRule = {
|
|||
timestampOverride: '',
|
||||
threat: threatDefault,
|
||||
note: '',
|
||||
setup: '',
|
||||
};
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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...',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -93,6 +93,7 @@ export const stepAboutDefaultValue: AboutStepRule = {
|
|||
timestampOverride: '',
|
||||
threat: threatDefault,
|
||||
note: '',
|
||||
setup: '',
|
||||
threatIndicatorPath: undefined,
|
||||
timestampOverrideFallbackDisabled: undefined,
|
||||
};
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ?? [],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue