From 95bcdeca972efae35e0e33bf8594b79497c78d78 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 12 Mar 2020 11:32:37 -0400 Subject: [PATCH 1/9] adding new note markdown value to create and edit pages --- .../detection_engine/rules/types.ts | 2 + .../rules/all/__mocks__/mock.ts | 153 +++++ .../__snapshots__/index.test.tsx.snap | 444 ++++++++++++++ .../description_step/helpers.test.tsx | 399 ++++++++++++ .../components/description_step/helpers.tsx | 43 +- .../description_step/index.test.tsx | 298 ++++++++- .../components/description_step/index.tsx | 58 +- .../step_about_rule/default_value.ts | 1 + .../components/step_about_rule/index.test.tsx | 165 +++++ .../components/step_about_rule/index.tsx | 138 +++-- .../components/step_about_rule/schema.tsx | 20 +- .../step_about_rule/translations.ts | 7 + .../components/step_define_rule/index.tsx | 4 +- .../components/step_schedule_rule/index.tsx | 4 +- .../rules/create/helpers.test.ts | 573 ++++++++++++++++++ .../detection_engine/rules/create/helpers.ts | 20 +- .../detection_engine/rules/create/index.tsx | 6 +- .../detection_engine/rules/details/index.tsx | 6 +- .../detection_engine/rules/helpers.test.tsx | 220 +++++++ .../pages/detection_engine/rules/helpers.tsx | 3 +- .../pages/detection_engine/rules/types.ts | 4 +- 21 files changed, 2464 insertions(+), 104 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index 4d2aec4ee87403..f962204c6b1b4d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -33,6 +33,7 @@ export const NewRuleSchema = t.intersection([ threat: t.array(t.unknown), to: t.string, updated_by: t.string, + note: t.string, }), ]); @@ -86,6 +87,7 @@ export const RuleSchema = t.intersection([ status_date: t.string, timeline_id: t.string, timeline_title: t.string, + note: t.string, version: t.number, }), ]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index e2287e5eeeb3fc..aeb254b7905109 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -4,7 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; +import { AboutStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; +import { FieldValueQueryBar } from '../../components/query_bar'; + +export const mockQueryBar: FieldValueQueryBar = { + query: { + query: 'test query', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; export const mockRule = (id: string): Rule => ({ created_at: '2020-01-10T21:11:45.839Z', @@ -37,9 +70,129 @@ export const mockRule = (id: string): Rule => ({ to: 'now', type: 'saved_query', threat: [], + note: '# this is some markdown documentation', + version: 1, +}); + +export const mockRuleWithEverything = (id: string): Rule => ({ + created_at: '2020-01-10T21:11:45.839Z', + updated_at: '2020-01-10T21:11:45.839Z', + created_by: 'elastic', + description: '24/7', + enabled: true, + false_positives: ['test'], + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + from: 'now-300s', + id, + immutable: false, + index: ['auditbeat-*'], + interval: '5m', + rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 21, + name: 'Query with rule-id', + query: 'user.name: root or user.name: admin', + references: ['www.test.co'], + saved_id: 'test123', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + meta: { from: '0m' }, + severity: 'low', + updated_by: 'elastic', + tags: ['tag1', 'tag2'], + to: 'now', + type: 'saved_query', + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + note: '# this is some markdown documentation', version: 1, }); +export const mockAboutStepRule: AboutStepRule = { + isNew: false, + name: 'Query with rule-id', + description: '24/7', + severity: 'low', + riskScore: 21, + references: ['www.test.co'], + falsePositives: ['test'], + tags: ['tag1', 'tag2'], + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + note: '# this is some markdown documentation', +}; + +export const mockDefineStepRule: DefineStepRule = { + isNew: false, + index: ['filebeat-'], + queryBar: mockQueryBar, +}; + +export const mockScheduleStepRule: ScheduleStepRule = { + isNew: false, + enabled: false, + interval: '5m', + from: '6m', + to: 'now', +}; + export const mockRuleError = (id: string): RuleError => ({ rule_id: id, error: { status_code: 404, message: `id: "${id}" not found` }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000000..050a8036b1333e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap @@ -0,0 +1,444 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is \`multi\` 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 1, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + ] + } + /> + + + + + + www.test.co + + + , + "title": "Reference URLs", + }, + Object { + "description":
    +
  • + test +
  • +
, + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
+ # test documentation +
+
, + "title": "Investigation notes", + }, + ] + } + /> +
+
+`; + +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is \`single\` 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 1, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + Object { + "description": + + + www.test.co + + + , + "title": "Reference URLs", + }, + Object { + "description":
    +
  • + test +
  • +
, + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
+ # test documentation +
+
, + "title": "Investigation notes", + }, + ] + } + /> +
+
+`; + +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is \`singleSplit 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 1, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + Object { + "description": + + + www.test.co + + + , + "title": "Reference URLs", + }, + Object { + "description":
    +
  • + test +
  • +
, + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
+ # test documentation +
+
, + "title": "Investigation notes", + }, + ] + } + type="column" + /> +
+
+`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx new file mode 100644 index 00000000000000..5f28d201fc6b5c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx @@ -0,0 +1,399 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +import { coreMock } from '../../../../../../../../../../src/core/public/mocks'; +import { esFilters, FilterManager } from '../../../../../../../../../../src/plugins/data/public'; +import { SeverityBadge } from '../severity_badge'; + +import * as i18n from './translations'; +import { + isNotEmptyArray, + buildQueryBarDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildStringArrayDescription, + buildSeverityDescription, + buildUrlsDescription, + buildNoteDescription, +} from './helpers'; +import { ListItems } from './types'; + +const setupMock = coreMock.createSetup(); +const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } +}; +setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); +const mockFilterManager = new FilterManager(setupMock.uiSettings); + +const mockQueryBar = { + query: { + query: 'test query', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +describe('helpers', () => { + describe('isNotEmptyArray', () => { + test('returns false if empty array', () => { + const result = isNotEmptyArray([]); + expect(result).toBeFalsy(); + }); + + test('returns false if array of empty strings', () => { + const result = isNotEmptyArray(['', '']); + expect(result).toBeFalsy(); + }); + + test('returns true if array of string with space', () => { + const result = isNotEmptyArray([' ']); + expect(result).toBeTruthy(); + }); + + test('returns true if array with at least one non-empty string', () => { + const result = isNotEmptyArray(['', 'abc']); + expect(result).toBeTruthy(); + }); + }); + + describe('buildQueryBarDescription', () => { + test('returns empty array if no filters, query or savedId exist', () => { + const emptyMockQueryBar = { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: emptyMockQueryBar.filters, + filterManager: mockFilterManager, + query: emptyMockQueryBar.query, + savedId: emptyMockQueryBar.saved_id, + }); + expect(result).toEqual([]); + }); + + test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: { + query: '', + language: 'kuery', + }, + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + }); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + }); + + test('returns expected array of ListItems when filters AND indexPatterns exist', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: { + query: '', + language: 'kuery', + }, + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, + }); + const wrapper = shallow(result[0].description as React.ReactElement); + const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); + expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); + expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); + }); + + test('returns expected array of ListItems when `query.query` exists', () => { + const mockQueryBarWithQuery = { + ...mockQueryBar, + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithQuery.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithQuery.query, + savedId: mockQueryBarWithQuery.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query.query} ); + }); + + test('returns expected array of ListItems when `savedId` exists', () => { + const mockQueryBarWithSavedId = { + ...mockQueryBar, + query: { + query: '', + language: 'kuery', + }, + filters: [], + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithSavedId.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithSavedId.query, + savedId: mockQueryBarWithSavedId.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} ); + }); + }); + + describe('buildThreatDescription', () => { + test('returns empty array if no threats', () => { + const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); + expect(result).toHaveLength(0); + }); + + // Not sure that this is the desired functionality, but just added tests per existing logic + test('returns empty tactic link if no corresponding tactic id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + // Not sure that this is the desired functionality, but just added tests per existing logic + test('returns empty technique link if no corresponding technique id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); + }); + + test('returns with corresponding tactic and technique link text', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns corresponding number of tactic and technique links', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, + { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, + ], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, + ], + tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); + }); + }); + + describe('buildUnorderedListArrayDescription', () => { + test('returns empty array if `values` is empty array', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + [] + ); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + ['', 'falsePositive1', 'falsePositive2'] + ); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); + }); + }); + + describe('buildStringArrayDescription', () => { + test('returns empty array if `values` is empty array', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ + '', + 'tag1', + 'tag2', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .first() + .text() + ).toEqual('tag1'); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .at(1) + .text() + ).toEqual('tag2'); + }); + }); + + describe('buildSeverityDescription', () => { + test('returns ListItem with passed in label and SeverityBadge component', () => { + const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + + expect(result[0].title).toEqual('Test label'); + expect(result[0].description).toEqual(); + }); + }); + + describe('buildUrlsDescription', () => { + test('returns empty array if `values` is empty array', () => { + const result: ListItems[] = buildUrlsDescription('Test label', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUrlsDescription('Test label', [ + 'www.test.com', + 'www.test2.com', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .first() + .text() + ).toEqual('www.test.com'); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .at(1) + .text() + ).toEqual('www.test2.com'); + }); + }); + + describe('buildNoteDescription', () => { + test('returns ListItem with passed in label and SeverityBadge component', () => { + const noteSample = + 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; + const result: ListItems[] = buildNoteDescription('Test label', noteSample); + const wrapper = shallow(result[0].description as React.ReactElement); + const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); + + expect(result[0].title).toEqual('Test label'); + expect(noteElement.exists()).toBeTruthy(); + expect(noteElement.text()).toEqual(noteSample); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index df767fbd4ff8cd..dc5fb9f8855365 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -27,7 +27,12 @@ import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './t import { SeverityBadge } from '../severity_badge'; import ListTreeIcon from './assets/list_tree_icon.svg'; -const isNotEmptyArray = (values: string[]) => +const NoteDescriptionContainer = styled(EuiFlexItem)` + height: 105px; + overflow-y: hidden; +`; + +export const isNotEmptyArray = (values: string[]) => !isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0; const EuiBadgeWrap = styled(EuiBadge)` @@ -124,7 +129,11 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription const tactic = tacticsOptions.find(t => t.id === singleThreat.tactic.id); return ( - + {tactic != null ? tactic.text : ''} @@ -133,6 +142,7 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription return ( {values.map((val: string) => - isEmpty(val) ? null :
  • {val}
  • + isEmpty(val) ? null : ( +
  • + {val} +
  • + ) )} ), @@ -193,7 +207,9 @@ export const buildStringArrayDescription = ( {values.map((val: string) => isEmpty(val) ? null : ( - {val} + + {val} + ) )} @@ -227,6 +243,7 @@ export const buildUrlsDescription = (label: string, values: string[]): ListItems iconType="link" size="xs" flush="left" + data-test-subj="urlsDescriptionReferenceLinkItem" > {val} @@ -239,3 +256,21 @@ export const buildUrlsDescription = (label: string, values: string[]): ListItems } return []; }; + +export const buildNoteDescription = (label: string, note: string): ListItems[] => { + if (note) { + return [ + { + title: label, + description: ( + +
    + {note} +
    +
    + ), + }, + ]; + } + return []; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx index 84c662dd001992..d319c28403e850 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx @@ -3,12 +3,89 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import { shallow } from 'enzyme'; -import { addFilterStateIfNotThere } from './'; +import { + StepRuleDescriptionComponent, + addFilterStateIfNotThere, + buildListItems, + getDescriptionItem, +} from './'; -import { esFilters, Filter } from '../../../../../../../../../../src/plugins/data/public'; +import { + esFilters, + Filter, + FilterManager, +} from '../../../../../../../../../../src/plugins/data/public'; +import { mockAboutStepRule } from '../../all/__mocks__/mock'; +import { coreMock } from '../../../../../../../../../../src/core/public/mocks'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; +import * as i18n from './translations'; + +import { schema } from '../step_about_rule/schema'; +import { ListItems } from './types'; describe('description_step', () => { + const setupMock = coreMock.createSetup(); + const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } + }; + let mockFilterManager: FilterManager; + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); + mockFilterManager = new FilterManager(setupMock.uiSettings); + }); + + describe('StepRuleDescriptionComponent', () => { + test('renders correctly against snapshot when columns is `multi`', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); + }); + + test('renders correctly against snapshot when columns is `single`', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + }); + + test('renders correctly against snapshot when columns is `singleSplit', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + expect( + wrapper + .find('[data-test-subj="singleSplitStepRuleDescriptionList"]') + .at(0) + .prop('type') + ).toEqual('column'); + }); + }); + describe('addFilterStateIfNotThere', () => { test('it does not change the state if it is global', () => { const filters: Filter[] = [ @@ -182,4 +259,221 @@ describe('description_step', () => { expect(output).toEqual(expected); }); }); + + describe('buildListItems', () => { + test('returns expected ListItems array when given valid inputs', () => { + const result: ListItems[] = buildListItems(mockAboutStepRule, schema, mockFilterManager); + + expect(result.length).toEqual(10); + }); + }); + + describe('getDescriptionItem', () => { + test('returns ListItem with all values enumerated when value[field] is an array', () => { + const result: ListItems[] = getDescriptionItem( + 'tags', + 'Tags label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('Tags label'); + expect(typeof result[0].description).toEqual('object'); + }); + + test('returns ListItem with description of value[field] when value[field] is a string', () => { + const result: ListItems[] = getDescriptionItem( + 'description', + 'Description label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('Description label'); + expect(result[0].description).toEqual('24/7'); + }); + + test('returns empty array when `value` is a non-existant property in `field`', () => { + const result: ListItems[] = getDescriptionItem( + 'jibberjabber', + 'JibberJabber label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + + describe('queryBar', () => { + test('returns array of ListItems when queryBar exist', () => { + const mockQueryBar = { + isNew: false, + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: null, + saved_id: null, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'queryBar', + 'Query bar label', + mockQueryBar, + mockFilterManager + ); + + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} ); + }); + }); + + describe('threat', () => { + test('returns array of ListItems when threat exist', () => { + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threat label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + + test('filters out threats with tactic.name of `none`', () => { + const mockAboutStep = { + ...mockAboutStepRule, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockAboutStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + }); + + describe('references', () => { + test('returns array of ListItems when references exist', () => { + const result: ListItems[] = getDescriptionItem( + 'references', + 'Reference label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('Reference label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('falsePositives', () => { + test('returns array of ListItems when falsePositives exist', () => { + const result: ListItems[] = getDescriptionItem( + 'falsePositives', + 'False positives label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('False positives label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('severity', () => { + test('returns array of ListItems when severity exist', () => { + const result: ListItems[] = getDescriptionItem( + 'severity', + 'Severity label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('Severity label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('riskScore', () => { + test('returns array of ListItems when riskScore exist', () => { + const result: ListItems[] = getDescriptionItem( + 'riskScore', + 'Risk score label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('Risk score label'); + expect(result[0].description).toEqual(1); + }); + }); + + describe('timeline', () => { + test('returns timeline title if one exists', () => { + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual('Titled timeline'); + }); + + test('returns default timeline title if none exists', () => { + const mockAboutStep = { + ...mockAboutStepRule, + timeline: { + id: '12345', + }, + }; + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); + }); + }); + + describe('note', () => { + test('returns default `note` description', () => { + const result: ListItems[] = getDescriptionItem( + 'note', + 'Investigation notes', + mockAboutStepRule, + mockFilterManager + ); + + expect(result[0].title).toEqual('Investigation notes'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index cb5c98bb23f07f..a03e1ae8155720 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -7,6 +7,7 @@ import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty, chunk, get, pick } from 'lodash/fp'; import React, { memo, useState } from 'react'; +import styled from 'styled-components'; import { IIndexPattern, @@ -28,18 +29,28 @@ import { buildThreatDescription, buildUnorderedListArrayDescription, buildUrlsDescription, + buildNoteDescription, } from './helpers'; +const DescriptionListContainer = styled(EuiDescriptionList)` + &.euiDescriptionList--column .euiDescriptionList__title { + width: 25%; + } + &.euiDescriptionList--column .euiDescriptionList__description { + width: 75%; + } +`; + interface StepRuleDescriptionProps { - direction?: 'row' | 'column'; + columns?: 'multi' | 'single' | 'singleSplit'; data: unknown; indexPatterns?: IIndexPattern; schema: FormSchema; } -const StepRuleDescriptionComponent: React.FC = ({ +export const StepRuleDescriptionComponent: React.FC = ({ data, - direction = 'row', + columns = 'multi', indexPatterns, schema, }) => { @@ -55,11 +66,14 @@ const StepRuleDescriptionComponent: React.FC = ({ [] ); - if (direction === 'row') { + if (columns === 'multi') { return ( {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( - + ))} @@ -69,8 +83,16 @@ const StepRuleDescriptionComponent: React.FC = ({ return ( - - + + {columns === 'single' ? ( + + ) : ( + + )} ); @@ -78,7 +100,7 @@ const StepRuleDescriptionComponent: React.FC = ({ export const StepRuleDescription = memo(StepRuleDescriptionComponent); -const buildListItems = ( +export const buildListItems = ( data: unknown, schema: FormSchema, filterManager: FilterManager, @@ -108,7 +130,7 @@ export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { }); }; -const getDescriptionItem = ( +export const getDescriptionItem = ( field: string, label: string, value: unknown, @@ -132,13 +154,6 @@ const getDescriptionItem = ( (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' ); return buildThreatDescription({ label, threat }); - } else if (field === 'description') { - return [ - { - title: label, - description: get(field, value), - }, - ]; } else if (field === 'references') { const urls: string[] = get(field, value); return buildUrlsDescription(label, urls); @@ -166,14 +181,9 @@ const getDescriptionItem = ( description: timeline.title ?? DEFAULT_TIMELINE_TITLE, }, ]; - } else if (field === 'riskScore') { - const description: string = get(field, value); - return [ - { - title: label, - description, - }, - ]; + } else if (field === 'note') { + const val: string = get(field, value); + return buildNoteDescription(label, val); } const description: string = get(field, value); if (!isEmpty(description)) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts index d15cce15877b46..417133f230610f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts @@ -29,4 +29,5 @@ export const stepAboutDefaultValue: AboutStepRule = { title: DEFAULT_TIMELINE_TITLE, }, threat: threatDefault, + note: '', }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx new file mode 100644 index 00000000000000..81a2ee59a30f36 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { StepAboutRule } from './'; +import { mockAboutStepRule } from '../../all/__mocks__/mock'; +import { StepRuleDescription } from '../description_step'; +import { stepAboutDefaultValue } from './default_value'; + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +describe('StepAboutRuleComponent', () => { + test('it renders StepRuleDescription if isReadOnlyView is true and `name` property exists', () => { + // see mockAboutStepRule for name property + // Note: is the name check for old rules? It's required so no rules should ever not include it + const wrapper = shallow( + + ); + + expect(wrapper.find(StepRuleDescription).exists()).toBeTruthy(); + }); + + test('it prevents user from clicking continue if no `description` defined', () => { + const wrapper = mount( + + + + ); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + nameInput.simulate('change', { target: { value: 'Test name text' } }); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + + expect( + wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0) + .props().value + ).toEqual('Test name text'); + expect(descriptionInput.props().value).toEqual(''); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] label') + .at(0) + .hasClass('euiFormLabel-isInvalid') + ).toBeTruthy(); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] EuiTextArea') + .at(0) + .prop('isInvalid') + ).toBeTruthy(); + }); + + test('it prevents user from clicking continue if no `name` defined', () => { + const wrapper = mount( + + + + ); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + descriptionInput.simulate('change', { target: { value: 'Test description text' } }); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + + expect( + wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0) + .props().value + ).toEqual('Test description text'); + expect(nameInput.props().value).toEqual(''); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] label') + .at(0) + .hasClass('euiFormLabel-isInvalid') + ).toBeTruthy(); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] EuiFieldText') + .at(0) + .prop('isInvalid') + ).toBeTruthy(); + }); + + test('it allows user to click continue if `name` and `description` are defined', () => { + const wrapper = mount( + + + + ); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + descriptionInput.simulate('change', { target: { value: 'Test description text' } }); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + nameInput.simulate('change', { target: { value: 'Test name text' } }); + + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + }); + + describe('advanced settings', () => { + test('it renders advanced settings collapsed initially', () => {}); + + test('it expands to show advanced fields', () => {}); + }); + + describe('submission', () => {}); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 4f06d4314c1f35..d7678f1975f009 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -39,6 +39,7 @@ import { schema } from './schema'; import * as I18n from './translations'; import { PickTimeline } from '../pick_timeline'; import { StepContentWrapper } from '../step_content_wrapper'; +import { MarkdownEditorForm } from '../../../../../components/markdown_editor/form'; const CommonUseField = getUseField({ component: Field }); @@ -46,6 +47,12 @@ interface StepAboutRuleProps extends RuleStepProps { defaultValues?: AboutStepRule | null; } +const ThreeQuartersContainer = styled.div` + max-width: 740px; +`; + +ThreeQuartersContainer.displayName = 'ThreeQuartersContainer'; + const TagContainer = styled.div` margin-top: 16px; `; @@ -75,7 +82,7 @@ const AdvancedSettingsAccordionButton = ( const StepAboutRuleComponent: FC = ({ addPadding = false, defaultValues, - descriptionDirection = 'row', + descriptionColumns = 'singleSplit', isReadOnlyView, isUpdateView = false, isLoading, @@ -121,67 +128,73 @@ const StepAboutRuleComponent: FC = ({ return isReadOnlyView && myStepData.name != null ? ( - + ) : ( <>
    - - + + + - - - - - - - - + + + + + + + + + + + = ({ dataTestSubj: 'detectionEngineStepAboutRuleMitreThreat', }} /> + + + + {({ severity }) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 42cf1e0d956499..7c1ab09b7309c4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -95,7 +95,14 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', { - defaultMessage: 'Investigate detections using this timeline template', + defaultMessage: 'Timeline template', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', + { + defaultMessage: + 'Select an existing timeline to use as a template when investigating generated signals.', } ), }, @@ -184,4 +191,15 @@ export const schema: FormSchema = { ), labelAppend: OptionalFieldLabel, }, + note: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.noteLabel', { + defaultMessage: 'Investigation notes', + }), + helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.noteHelpText', { + defaultMessage: + 'Provide helpful information for analysts that are performing a signal investigation. These notes will appear on both the rule details page and in timelines created from signals generated by this rule.', + }), + labelAppend: OptionalFieldLabel, + }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts index 3b6680fd4e6875..dfa60268e903aa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts @@ -68,3 +68,10 @@ export const URL_FORMAT_INVALID = i18n.translate( defaultMessage: 'Url is invalid format', } ); + +export const ADD_RULE_NOTE_HELP_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutrule.noteHelpText', + { + defaultMessage: 'Add rule investigation notes...', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 490a8d9d194cbb..5064f9d3bae9e4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -87,7 +87,7 @@ const getStepDefaultValue = ( const StepDefineRuleComponent: FC = ({ addPadding = false, defaultValues, - descriptionDirection = 'row', + descriptionColumns = 'singleSplit', isReadOnlyView, isLoading, isUpdateView = false, @@ -157,7 +157,7 @@ const StepDefineRuleComponent: FC = ({ return isReadOnlyView && myStepData?.queryBar != null ? ( = ({ addPadding = false, defaultValues, - descriptionDirection = 'row', + descriptionColumns = 'singleSplit', isReadOnlyView, isLoading, isUpdateView = false, @@ -80,7 +80,7 @@ const StepScheduleRuleComponent: FC = ({ return isReadOnlyView && myStepData != null ? ( - + ) : ( <> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts new file mode 100644 index 00000000000000..a968fc8074a741 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts @@ -0,0 +1,573 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { NewRule } from '../../../../containers/detection_engine/rules'; +import { DefineStepRuleJson, ScheduleStepRuleJson, AboutStepRuleJson } from '../types'; +import { + getTimeTypeValue, + formatDefineStepData, + formatScheduleStepData, + formatAboutStepData, + formatRule, +} from './helpers'; +import { + mockDefineStepRule, + mockQueryBar, + mockScheduleStepRule, + mockAboutStepRule, +} from '../all/__mocks__/mock'; + +describe('helpers', () => { + describe('getTimeTypeValue', () => { + test('returns timeObj with value 0 if no time value found', () => { + const result = getTimeTypeValue('m'); + + expect(result).toEqual({ unit: 'm', value: 0 }); + }); + + test('returns timeObj with unit set to empty string if no expected time type found', () => { + const result = getTimeTypeValue('5l'); + + expect(result).toEqual({ unit: '', value: 5 }); + }); + + test('returns timeObj with unit of s and value 5 when time is 5s ', () => { + const result = getTimeTypeValue('5s'); + + expect(result).toEqual({ unit: 's', value: 5 }); + }); + + test('returns timeObj with unit of m and value 5 when time is 5m ', () => { + const result = getTimeTypeValue('5m'); + + expect(result).toEqual({ unit: 'm', value: 5 }); + }); + + test('returns timeObj with unit of h and value 5 when time is 5h ', () => { + const result = getTimeTypeValue('5h'); + + expect(result).toEqual({ unit: 'h', value: 5 }); + }); + + test('returns timeObj with value of 5 when time is float like 5.6m ', () => { + const result = getTimeTypeValue('5m'); + + expect(result).toEqual({ unit: 'm', value: 5 }); + }); + + test('returns timeObj with value of 0 and unit of `` if random string passed in', () => { + const result = getTimeTypeValue('random'); + + expect(result).toEqual({ unit: '', value: 0 }); + }); + }); + + describe('formatDefineStepData', () => { + test('returns formatted object as DefineStepRuleJson', () => { + const result: DefineStepRuleJson = formatDefineStepData(mockDefineStepRule); + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + saved_id: 'test123', + index: ['filebeat-'], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with no saved_id if no savedId provided', () => { + const mockStepData = { + ...mockDefineStepRule, + queryBar: { + ...mockDefineStepRule.queryBar, + saved_id: '', + }, + }; + const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + index: ['filebeat-'], + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatScheduleStepData', () => { + test('returns formatted object as ScheduleStepRuleJson', () => { + const result: ScheduleStepRuleJson = formatScheduleStepData(mockScheduleStepRule); + const expected = { + enabled: false, + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with `to` as `now` if `to` not supplied', () => { + const mockStepData = { + ...mockScheduleStepRule, + }; + delete mockStepData.to; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + enabled: false, + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with `to` as `now` if `to` random string', () => { + const mockStepData = { + ...mockScheduleStepRule, + to: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + enabled: false, + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + // Note: do we want from to default to anything if it somehow ends up being unparsable string? + test('returns formatted object if `from` random string', () => { + const mockStepData = { + ...mockScheduleStepRule, + from: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + enabled: false, + from: 'now-300s', + to: 'now', + interval: '5m', + meta: { + from: 'random', + }, + }; + + expect(result).toEqual(expected); + }); + + // Note: do we want interval to default to anything if it somehow ends up being unparsable string? + test('returns formatted object if `interval` random string', () => { + const mockStepData = { + ...mockScheduleStepRule, + interval: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + enabled: false, + from: 'now-360s', + to: 'now', + interval: 'random', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatAboutStepData', () => { + test('returns formatted object as AboutStepRuleJson', () => { + const result: AboutStepRuleJson = formatAboutStepData(mockAboutStepRule); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# test documentation', + references: ['www.test.co'], + risk_score: 1, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with empty falsePositive and references filtered out', () => { + const mockStepData = { + ...mockAboutStepRule, + falsePositives: ['', 'test', ''], + references: ['www.test.co', ''], + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# test documentation', + references: ['www.test.co'], + risk_score: 1, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without note if note is empty string', () => { + const mockStepData = { + ...mockAboutStepRule, + note: '', + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + references: ['www.test.co'], + risk_score: 1, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without timeline_id and timeline_title if timeline.id is null', () => { + const mockStepData = { + ...mockAboutStepRule, + }; + delete mockStepData.timeline.id; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# test documentation', + references: ['www.test.co'], + risk_score: 1, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + + expect(result).toEqual(expected); + }); + + // Note: is this desired behavior? Should we also check for empty string? + test('returns formatted object with timeline_id and timeline_title if timeline.id is ``', () => { + const mockStepData = { + ...mockAboutStepRule, + timeline: { + ...mockAboutStepRule.timeline, + id: '', + }, + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# test documentation', + references: ['www.test.co'], + risk_score: 1, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline_id: '', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without timeline_id and timeline_title if timeline.title is null', () => { + const mockStepData = { + ...mockAboutStepRule, + timeline: { + ...mockAboutStepRule.timeline, + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + }, + }; + delete mockStepData.timeline.title; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# test documentation', + references: ['www.test.co'], + risk_score: 1, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + + expect(result).toEqual(expected); + }); + + // Note: is this desired behavior? Should we also check for empty string? + test('returns formatted object with timeline_id and timeline_title if timeline.title is ``', () => { + const mockStepData = { + ...mockAboutStepRule, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: '', + }, + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# test documentation', + references: ['www.test.co'], + risk_score: 1, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: '1234', name: 'tactic1', reference: 'reference1' }, + technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], + }, + ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: '', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with threats filtered out where tactic.name is `none`', () => { + const mockStepData = { + ...mockAboutStepRule, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# test documentation', + references: ['www.test.co'], + risk_score: 1, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: '1234', name: 'tactic1', reference: 'reference1' }, + technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], + }, + ], + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatRule', () => { + test('returns NewRule with type of saved_query when saved_id exists', () => { + const result: NewRule = formatRule( + mockDefineStepRule, + mockAboutStepRule, + mockScheduleStepRule + ); + + expect(result.type).toEqual('saved_query'); + }); + + test('returns NewRule with type of query when saved_id does not exist', () => { + const mockDefineStepRuleWithoutSavedId = { + ...mockDefineStepRule, + queryBar: { + ...mockDefineStepRule.queryBar, + saved_id: '', + }, + }; + const result: NewRule = formatRule( + mockDefineStepRuleWithoutSavedId, + mockAboutStepRule, + mockScheduleStepRule + ); + + expect(result.type).toEqual('query'); + }); + + test('returns NewRule with id set to ruleId if ruleId exists', () => { + const result: NewRule = formatRule( + mockDefineStepRule, + mockAboutStepRule, + mockScheduleStepRule, + 'query-with-rule-id' + ); + + expect(result.id).toEqual('query-with-rule-id'); + }); + + test('returns NewRule without id if ruleId does not exist', () => { + const result: NewRule = formatRule( + mockDefineStepRule, + mockAboutStepRule, + mockScheduleStepRule + ); + + expect(result.id).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index de6678b42df6f2..07578e870bf2be 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -19,7 +19,7 @@ import { FormatRuleType, } from '../types'; -const getTimeTypeValue = (time: string): { unit: string; value: number } => { +export const getTimeTypeValue = (time: string): { unit: string; value: number } => { const timeObj = { unit: '', value: 0, @@ -39,7 +39,7 @@ const getTimeTypeValue = (time: string): { unit: string; value: number } => { return timeObj; }; -const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { +export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { const { queryBar, isNew, ...rest } = defineStepData; const { filters, query, saved_id: savedId } = queryBar; return { @@ -51,7 +51,7 @@ const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJso }; }; -const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { +export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { const { isNew, ...formatScheduleData } = scheduleData; if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( @@ -71,8 +71,17 @@ const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRul }; }; -const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threat, timeline, isNew, ...rest } = aboutStepData; +export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { + const { + falsePositives, + references, + riskScore, + threat, + timeline, + isNew, + note, + ...rest + } = aboutStepData; return { false_positives: falsePositives.filter(item => !isEmpty(item)), references: references.filter(item => !isEmpty(item)), @@ -93,6 +102,7 @@ const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => return { id, name, reference }; }), })), + ...(!isEmpty(note) ? { note } : {}), ...rest, }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index d816c7e867057c..c9f44ab0048f94 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -286,7 +286,7 @@ const CreateRulePageComponent: React.FC = () => { isLoading={isLoading || loading} setForm={setStepsForm} setStepData={setStepData} - descriptionDirection="row" + descriptionColumns="singleSplit" /> @@ -315,7 +315,7 @@ const CreateRulePageComponent: React.FC = () => { { defaultValues={ (stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule) ?? null } - descriptionDirection="row" + descriptionColumns="singleSplit" isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.scheduleRule]} isLoading={isLoading || loading} setForm={setStepsForm} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index e73852ec91287d..18c7c7abffd7ab 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -300,7 +300,7 @@ const RuleDetailsPageComponent: FC = ({ {defineRuleData != null && ( = ({ {aboutRuleData != null && ( = ({ {scheduleRuleData != null && ( { + describe('getStepsData', () => { + test('returns object with about, define, and schedule step properties formatted', () => { + const result = getStepsData({ rule: mockRuleWithEverything('test-id') }); + const defineRuleStepData = { + isNew: false, + index: ['auditbeat-*'], + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', + }, + }; + const aboutRuleStepData = { + description: '24/7', + falsePositives: ['test'], + isNew: false, + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + riskScore: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, + }; + const scheduleRuleStepData = { enabled: true, from: '0s', interval: '5m', isNew: false }; + + expect(result.defineRuleData).toEqual(defineRuleStepData); + expect(result.aboutRuleData).toEqual(aboutRuleStepData); + expect(result.scheduleRuleData).toEqual(scheduleRuleStepData); + }); + + describe('defineStepRule', () => { + test('returns with saved_id if value exists on rule', () => { + const result: GetStepsData = getStepsData({ rule: mockRule('test-id') }); + const expected = { + isNew: false, + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: "Garrett's IP", + }, + }; + + expect(result.defineRuleData).toEqual(expected); + }); + + test('returns with saved_id of null if value does not exist on rule', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + delete mockedRule.saved_id; + const result: GetStepsData = getStepsData({ rule: mockedRule }); + const expected = { + isNew: false, + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: null, + }, + }; + + expect(result.defineRuleData).toEqual(expected); + }); + }); + + describe('aboutRuleData', () => { + test('returns timeline id and title of null if they do not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.timeline_id; + delete mockedRule.timeline_title; + const result: GetStepsData = getStepsData({ rule: mockedRule }); + + expect(result.aboutRuleData?.timeline.id).toBeNull(); + expect(result.aboutRuleData?.timeline.title).toBeNull(); + }); + + test('returns name as empty string if detailsView is true', () => { + const result: GetStepsData = getStepsData({ + rule: mockRuleWithEverything('test-id'), + detailsView: true, + }); + + expect(result.aboutRuleData?.name).toEqual(''); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: GetStepsData = getStepsData({ + rule: mockedRule, + detailsView: true, + }); + + expect(result.aboutRuleData?.note).toEqual(''); + }); + }); + + describe('scheduleRuleData', () => { + test('returns from as seconds if from duration is less than a minute', () => { + const mockedRule = { + ...mockRule('test-id'), + from: 'now-62s', + interval: '1m', + }; + const result = getStepsData({ rule: mockedRule }); + + expect(result.scheduleRuleData?.from).toEqual('2s'); + expect(result.scheduleRuleData?.interval).toEqual('1m'); + }); + + test('returns from as minutes if from duration is less than an hour', () => { + const mockedRule = { + ...mockRule('test-id'), + from: 'now-660s', + }; + const result = getStepsData({ rule: mockedRule }); + + expect(result.scheduleRuleData?.from).toEqual('6m'); + expect(result.scheduleRuleData?.interval).toEqual('5m'); + }); + + test('returns from as hours if from duration is more than 60 minutes', () => { + const mockedRule = { + ...mockRule('test-id'), + from: 'now-7400s', + interval: '5m', + }; + const result = getStepsData({ rule: mockedRule }); + + expect(result.scheduleRuleData?.from).toEqual('1h'); + expect(result.scheduleRuleData?.interval).toEqual('5m'); + }); + + // Note: is this desired behavior? + test('returns from as if from is not parsable as dateMath', () => { + const mockedRule = { + ...mockRule('test-id'), + from: 'randomstring', + }; + const result = getStepsData({ rule: mockedRule }); + + expect(result.scheduleRuleData?.from).toEqual('NaNh'); + }); + + test('returns from as 5m if interval is not parsable as dateMath', () => { + const mockedRule = { + ...mockRule('test-id'), + interval: 'randomstring', + }; + const result = getStepsData({ rule: mockedRule }); + + expect(result.scheduleRuleData?.from).toEqual('5m'); + expect(result.scheduleRuleData?.interval).toEqual('randomstring'); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 85f3bcbd236e90..9368f08d63eaa4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -14,7 +14,7 @@ import { Rule } from '../../../containers/detection_engine/rules'; import { FormData, FormHook, FormSchema } from '../../../shared_imports'; import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; -interface GetStepsData { +export interface GetStepsData { aboutRuleData: AboutStepRule | null; defineRuleData: DefineStepRule | null; scheduleRuleData: ScheduleStepRule | null; @@ -52,6 +52,7 @@ export const getStepsData = ({ id: rule.timeline_id ?? null, title: rule.timeline_title ?? null, }, + note: rule.note ?? '', } : null; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index 34df20de1e461c..e3e4eca0fd7aee 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -36,7 +36,7 @@ export interface RuleStepData { export interface RuleStepProps { addPadding?: boolean; - descriptionDirection?: 'row' | 'column'; + descriptionColumns?: 'multi' | 'single' | 'singleSplit'; setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; isReadOnlyView: boolean; isUpdateView?: boolean; @@ -58,6 +58,7 @@ export interface AboutStepRule extends StepRuleData { tags: string[]; timeline: FieldValueTimeline; threat: IMitreEnterpriseAttack[]; + note: string; } export interface DefineStepRule extends StepRuleData { @@ -91,6 +92,7 @@ export interface AboutStepRuleJson { timeline_id?: string; timeline_title?: string; threat: IMitreEnterpriseAttack[]; + note?: string; } export interface ScheduleStepRuleJson { From 5ac18367a327db4d8d84fac9b5dbb36dbfd4a945 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Fri, 13 Mar 2020 09:43:25 -0400 Subject: [PATCH 2/9] add markdown to details page and update unit tests --- .../rules/all/__mocks__/mock.ts | 20 +- .../__snapshots__/index.test.tsx.snap | 234 ++++++------------ .../components/description_step/helpers.tsx | 59 ++--- .../description_step/index.test.tsx | 49 ++-- .../components/description_step/index.tsx | 4 +- .../components/step_about_rule/index.test.tsx | 2 +- .../step_about_rule_details/index.test.tsx | 205 +++++++++++++++ .../step_about_rule_details/index.tsx | 111 +++++++++ .../step_about_rule_details/translations.ts | 27 ++ .../components/step_schedule_rule/index.tsx | 47 ++-- .../rules/create/helpers.test.ts | 134 +++++----- .../detection_engine/rules/details/index.tsx | 60 ++--- .../detection_engine/rules/helpers.test.tsx | 88 ++++++- .../pages/detection_engine/rules/helpers.tsx | 41 ++- .../pages/detection_engine/rules/types.ts | 5 + .../pages/detection_engine/translations.ts | 2 +- 16 files changed, 752 insertions(+), 336 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index aeb254b7905109..5627d338185009 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -146,8 +146,8 @@ export const mockRuleWithEverything = (id: string): Rule => ({ version: 1, }); -export const mockAboutStepRule: AboutStepRule = { - isNew: false, +export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ + isNew, name: 'Query with rule-id', description: '24/7', severity: 'low', @@ -177,21 +177,21 @@ export const mockAboutStepRule: AboutStepRule = { }, ], note: '# this is some markdown documentation', -}; +}); -export const mockDefineStepRule: DefineStepRule = { - isNew: false, +export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ + isNew, index: ['filebeat-'], queryBar: mockQueryBar, -}; +}); -export const mockScheduleStepRule: ScheduleStepRule = { - isNew: false, - enabled: false, +export const mockScheduleStepRule = (isNew = false, enabled = false): ScheduleStepRule => ({ + isNew, + enabled, interval: '5m', from: '6m', to: 'now', -}; +}); export const mockRuleError = (id: string): RuleError => ({ rule_id: id, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap index 050a8036b1333e..fddb315e3e44d0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap @@ -24,7 +24,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against "title": "Severity", }, Object { - "description": 1, + "description": 21, "title": "Risk score", }, Object { @@ -43,67 +43,43 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against listItems={ Array [ Object { - "description": - - +
  • - www.test.co - - - , + + www.test.co + +
  • + + , "title": "Reference URLs", }, Object { - "description":
      -
    • - test -
    • -
    , + "description": +
      +
    • + test +
    • +
    +
    , "title": "False positive examples", }, Object { "description": - - - - - - - - - - - - , "title": "MITRE ATT&CK™", @@ -143,7 +119,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against className="eui-yScrollWithShadows" data-test-subj="noteDescriptionItem" > - # test documentation + # this is some markdown documentation , "title": "Investigation notes", @@ -178,7 +154,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against "title": "Severity", }, Object { - "description": 1, + "description": 21, "title": "Risk score", }, Object { @@ -186,67 +162,43 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against "title": "Timeline template", }, Object { - "description": - - +
  • - www.test.co - - - , + + www.test.co + +
  • + + , "title": "Reference URLs", }, Object { - "description":
      -
    • - test -
    • -
    , + "description": +
      +
    • + test +
    • +
    +
    , "title": "False positive examples", }, Object { "description": - - - - - - - - - - - - , "title": "MITRE ATT&CK™", @@ -286,7 +238,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against className="eui-yScrollWithShadows" data-test-subj="noteDescriptionItem" > - # test documentation + # this is some markdown documentation , "title": "Investigation notes", @@ -322,7 +274,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against "title": "Severity", }, Object { - "description": 1, + "description": 21, "title": "Risk score", }, Object { @@ -330,67 +282,43 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against "title": "Timeline template", }, Object { - "description": - - +
  • - www.test.co - - - , + + www.test.co + +
  • + + , "title": "Reference URLs", }, Object { - "description":
      -
    • - test -
    • -
    , + "description": +
      +
    • + test +
    • +
    +
    , "title": "False positive examples", }, Object { "description": - - - - - - - - - - - - , "title": "MITRE ATT&CK™", @@ -430,7 +358,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against className="eui-yScrollWithShadows" data-test-subj="noteDescriptionItem" > - # test documentation + # this is some markdown documentation , "title": "Investigation notes", diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index dc5fb9f8855365..2fa14a9e5c7e9a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -9,9 +9,10 @@ import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, - EuiLink, EuiButtonEmpty, EuiSpacer, + EuiLink, + EuiText, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; @@ -111,13 +112,6 @@ const TechniqueLinkItem = styled(EuiButtonEmpty)` } `; -const ReferenceLinkItem = styled(EuiButtonEmpty)` - .euiIcon { - width: 12px; - height: 12px; - } -`; - export const buildThreatDescription = ({ label, threat }: BuildThreatDescription): ListItems[] => { if (threat.length > 0) { return [ @@ -177,15 +171,17 @@ export const buildUnorderedListArrayDescription = ( { title: label, description: ( -
      - {values.map((val: string) => - isEmpty(val) ? null : ( -
    • - {val} -
    • - ) - )} -
    + +
      + {values.map((val: string) => + isEmpty(val) ? null : ( +
    • + {val} +
    • + ) + )} +
    +
    ), }, ]; @@ -234,22 +230,19 @@ export const buildUrlsDescription = (label: string, values: string[]): ListItems { title: label, description: ( - - {values.map((val: string) => ( - - - {val} - - - ))} - + +
      + {values + .filter(v => !isEmpty(v)) + .map((val: string, index: number) => ( +
    • + + {val} + +
    • + ))} +
    +
    ), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx index d319c28403e850..ba23d27c6c8c58 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx @@ -25,6 +25,7 @@ import * as i18n from './translations'; import { schema } from '../step_about_rule/schema'; import { ListItems } from './types'; +import { AboutStepRule } from '../../types'; describe('description_step', () => { const setupMock = coreMock.createSetup(); @@ -37,6 +38,7 @@ describe('description_step', () => { } }; let mockFilterManager: FilterManager; + let mockAboutStep: AboutStepRule; beforeEach(() => { // jest carries state between mocked implementations when using @@ -48,12 +50,13 @@ describe('description_step', () => { setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); mockFilterManager = new FilterManager(setupMock.uiSettings); + mockAboutStep = mockAboutStepRule(); }); describe('StepRuleDescriptionComponent', () => { test('renders correctly against snapshot when columns is `multi`', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); @@ -61,7 +64,7 @@ describe('description_step', () => { test('renders correctly against snapshot when columns is `single`', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); @@ -69,11 +72,7 @@ describe('description_step', () => { test('renders correctly against snapshot when columns is `singleSplit', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); @@ -262,7 +261,7 @@ describe('description_step', () => { describe('buildListItems', () => { test('returns expected ListItems array when given valid inputs', () => { - const result: ListItems[] = buildListItems(mockAboutStepRule, schema, mockFilterManager); + const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); expect(result.length).toEqual(10); }); @@ -273,7 +272,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'tags', 'Tags label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); @@ -285,7 +284,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'description', 'Description label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); @@ -297,7 +296,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'jibberjabber', 'JibberJabber label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); @@ -334,7 +333,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'threat', 'Threat label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); @@ -343,8 +342,8 @@ describe('description_step', () => { }); test('filters out threats with tactic.name of `none`', () => { - const mockAboutStep = { - ...mockAboutStepRule, + const mockStep = { + ...mockAboutStep, threat: [ { framework: 'mockFramework', @@ -366,7 +365,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'threat', 'Threat label', - mockAboutStep, + mockStep, mockFilterManager ); @@ -379,7 +378,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'references', 'Reference label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); @@ -393,7 +392,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'falsePositives', 'False positives label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); @@ -407,7 +406,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'severity', 'Severity label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); @@ -421,12 +420,12 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'riskScore', 'Risk score label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); expect(result[0].title).toEqual('Risk score label'); - expect(result[0].description).toEqual(1); + expect(result[0].description).toEqual(21); }); }); @@ -435,7 +434,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'timeline', 'Timeline label', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); @@ -444,8 +443,8 @@ describe('description_step', () => { }); test('returns default timeline title if none exists', () => { - const mockAboutStep = { - ...mockAboutStepRule, + const mockStep = { + ...mockAboutStep, timeline: { id: '12345', }, @@ -453,7 +452,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'timeline', 'Timeline label', - mockAboutStep, + mockStep, mockFilterManager ); @@ -467,7 +466,7 @@ describe('description_step', () => { const result: ListItems[] = getDescriptionItem( 'note', 'Investigation notes', - mockAboutStepRule, + mockAboutStep, mockFilterManager ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index a03e1ae8155720..1d58ef8014899a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -34,10 +34,10 @@ import { const DescriptionListContainer = styled(EuiDescriptionList)` &.euiDescriptionList--column .euiDescriptionList__title { - width: 25%; + width: 30%; } &.euiDescriptionList--column .euiDescriptionList__description { - width: 75%; + width: 70%; } `; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx index 81a2ee59a30f36..4654a2413188ce 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx @@ -22,7 +22,7 @@ describe('StepAboutRuleComponent', () => { const wrapper = shallow( ({ eui: euiDarkVars, darkMode: true }); + +describe('StepAboutRuleToggleDetails', () => { + let mockRule: AboutStepRule; + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + mockRule = mockAboutStepRule(); + }); + + test('it renders loading component when `loading` is true', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiProgress).exists()).toBeTruthy(); + expect(wrapper.find(HeaderSection).exists()).toBeTruthy(); + }); + + test('it does not render details if stepDataDetails is null', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); + }); + + test('it does not render details if stepData is null', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); + }); + + describe('note value does NOT exist', () => { + test('it does not render toggle buttons', () => { + const mockAboutStepWithoutNote = { + ...mockRule, + note: '', + }; + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiButtonGroup).exists()).toBeFalsy(); + expect( + wrapper + .find('EuiText[data-test-subj="stepAboutRuleDetailsToggleDescriptionText"] p') + .at(0) + .text() + ).toEqual(mockAboutStepWithoutNote.description); + }); + + test('it does not render description as part of the description list', () => { + const mockAboutStepWithoutNote = { + ...mockRule, + note: '', + }; + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="stepAboutRuleDetailsToggleDescriptionText"]') + .at(0) + .text() + ).toEqual(mockAboutStepWithoutNote.description); + }); + }); + + describe('note value does exist', () => { + test('it renders toggle buttons, defaulted to `about`', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy(); + expect( + wrapper + .find('EuiButtonToggle[id="about"]') + .at(0) + .prop('isSelected') + ).toBeTruthy(); + expect( + wrapper + .find('EuiButtonToggle[id="notes"]') + .at(0) + .prop('isSelected') + ).toBeFalsy(); + }); + + test('it allows users to toggle between `about` and `note`', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('EuiButtonGroup[idSelected="about"]').exists()).toBeTruthy(); + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy(); + + wrapper + .find('input[title="Investigation notes"]') + .at(0) + .simulate('change', { target: { value: 'notes' } }); + + expect(wrapper.find('EuiButtonGroup[idSelected="about"]').exists()).toBeFalsy(); + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); + }); + + test('it displays notes markdown when user toggles to `notes`', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('input[title="Investigation notes"]') + .at(0) + .simulate('change', { target: { value: 'notes' } }); + + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); + expect(wrapper.find('Markdown h1').text()).toEqual('this is some markdown documentation'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx new file mode 100644 index 00000000000000..ca6a1ca5063e31 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiPanel, + EuiProgress, + EuiButtonGroup, + EuiButtonGroupOption, + EuiSpacer, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import React, { memo, useState } from 'react'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash/fp'; + +import { HeaderSection } from '../../../../../components/header_section'; +import { Markdown } from '../../../../../components/markdown'; +import { AboutStepRule, AboutStepRuleDetails } from '../../types'; +import * as i18n from './translations'; +import { StepAboutRule } from '../step_about_rule/'; + +const toggleOptions: EuiButtonGroupOption[] = [ + { + id: 'about', + label: i18n.ABOUT_PANEL_DETAILS_TAB, + }, + { + id: 'notes', + label: i18n.ABOUT_PANEL_NOTES_TAB, + }, +]; + +interface StepPanelProps { + stepData: AboutStepRule | null; + stepDataDetails: AboutStepRuleDetails | null; + loading: boolean; +} + +const MyPanel = styled(EuiPanel)` + position: relative; +`; + +const AboutDescriptionContainer = styled(EuiFlexItem)` + max-height: 550px; + overflow-y: hidden; +`; + +const AboutDescriptionScrollContainer = styled.div` + overflow-x: hidden; +`; + +const StepAboutRuleToggleDetailsComponent: React.FC = ({ + stepData, + stepDataDetails, + loading, +}) => { + const [selectedToggleOption, setToggleOption] = useState('about'); + + return ( + + {loading && ( + <> + + + + )} + {stepData != null && stepDataDetails != null && ( + <> + + {!isEmpty(stepDataDetails.note) && ( + { + setToggleOption(val); + }} + /> + )} + + + + {selectedToggleOption === 'about' ? ( + <> + + +

    {stepDataDetails.description}

    +
    + + + + ) : ( + + )} +
    +
    + + )} +
    + ); +}; + +export const StepAboutRuleToggleDetails = memo(StepAboutRuleToggleDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts new file mode 100644 index 00000000000000..fa725366210deb --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const ABOUT_PANEL_DETAILS_TAB = i18n.translate( + 'xpack.siem.detectionEngine.details.stepAboutRule.detailsLabel', + { + defaultMessage: 'Details', + } +); + +export const ABOUT_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.details.stepAboutRule.aboutText', + { + defaultMessage: 'About', + } +); + +export const ABOUT_PANEL_NOTES_TAB = i18n.translate( + 'xpack.siem.detectionEngine.details.stepAboutRule.investigationNotesLabel', + { + defaultMessage: 'Investigation notes', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index dd6293e7d660e8..e365443a79fb85 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -7,6 +7,7 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import deepEqual from 'fast-deep-equal'; +import styled from 'styled-components'; import { setFieldValue } from '../../helpers'; import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; @@ -21,6 +22,10 @@ interface StepScheduleRuleProps extends RuleStepProps { defaultValues?: ScheduleStepRule | null; } +const RestrictedWidthContainer = styled.div` + max-width: 300px; +`; + const stepScheduleDefaultValue = { enabled: true, interval: '5m', @@ -86,25 +91,29 @@ const StepScheduleRuleComponent: FC = ({ <> - - + + + + + + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts index a968fc8074a741..7843466f56cd76 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts @@ -5,7 +5,14 @@ */ import { NewRule } from '../../../../containers/detection_engine/rules'; -import { DefineStepRuleJson, ScheduleStepRuleJson, AboutStepRuleJson } from '../types'; +import { + DefineStepRuleJson, + ScheduleStepRuleJson, + AboutStepRuleJson, + AboutStepRule, + ScheduleStepRule, + DefineStepRule, +} from '../types'; import { getTimeTypeValue, formatDefineStepData, @@ -66,8 +73,14 @@ describe('helpers', () => { }); describe('formatDefineStepData', () => { + let mockData: DefineStepRule; + + beforeEach(() => { + mockData = mockDefineStepRule(); + }); + test('returns formatted object as DefineStepRuleJson', () => { - const result: DefineStepRuleJson = formatDefineStepData(mockDefineStepRule); + const result: DefineStepRuleJson = formatDefineStepData(mockData); const expected = { language: 'kuery', filters: mockQueryBar.filters, @@ -81,9 +94,9 @@ describe('helpers', () => { test('returns formatted object with no saved_id if no savedId provided', () => { const mockStepData = { - ...mockDefineStepRule, + ...mockData, queryBar: { - ...mockDefineStepRule.queryBar, + ...mockData.queryBar, saved_id: '', }, }; @@ -100,8 +113,14 @@ describe('helpers', () => { }); describe('formatScheduleStepData', () => { + let mockData: ScheduleStepRule; + + beforeEach(() => { + mockData = mockScheduleStepRule(); + }); + test('returns formatted object as ScheduleStepRuleJson', () => { - const result: ScheduleStepRuleJson = formatScheduleStepData(mockScheduleStepRule); + const result: ScheduleStepRuleJson = formatScheduleStepData(mockData); const expected = { enabled: false, from: 'now-660s', @@ -117,7 +136,7 @@ describe('helpers', () => { test('returns formatted object with `to` as `now` if `to` not supplied', () => { const mockStepData = { - ...mockScheduleStepRule, + ...mockData, }; delete mockStepData.to; const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); @@ -136,7 +155,7 @@ describe('helpers', () => { test('returns formatted object with `to` as `now` if `to` random string', () => { const mockStepData = { - ...mockScheduleStepRule, + ...mockData, to: 'random', }; const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); @@ -156,7 +175,7 @@ describe('helpers', () => { // Note: do we want from to default to anything if it somehow ends up being unparsable string? test('returns formatted object if `from` random string', () => { const mockStepData = { - ...mockScheduleStepRule, + ...mockData, from: 'random', }; const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); @@ -176,7 +195,7 @@ describe('helpers', () => { // Note: do we want interval to default to anything if it somehow ends up being unparsable string? test('returns formatted object if `interval` random string', () => { const mockStepData = { - ...mockScheduleStepRule, + ...mockData, interval: 'random', }; const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); @@ -195,15 +214,21 @@ describe('helpers', () => { }); describe('formatAboutStepData', () => { + let mockData: AboutStepRule; + + beforeEach(() => { + mockData = mockAboutStepRule(); + }); + test('returns formatted object as AboutStepRuleJson', () => { - const result: AboutStepRuleJson = formatAboutStepData(mockAboutStepRule); + const result: AboutStepRuleJson = formatAboutStepData(mockData); const expected = { description: '24/7', false_positives: ['test'], name: 'Query with rule-id', - note: '# test documentation', + note: '# this is some markdown documentation', references: ['www.test.co'], - risk_score: 1, + risk_score: 21, severity: 'low', tags: ['tag1', 'tag2'], threat: [ @@ -232,7 +257,7 @@ describe('helpers', () => { test('returns formatted object with empty falsePositive and references filtered out', () => { const mockStepData = { - ...mockAboutStepRule, + ...mockData, falsePositives: ['', 'test', ''], references: ['www.test.co', ''], }; @@ -241,9 +266,9 @@ describe('helpers', () => { description: '24/7', false_positives: ['test'], name: 'Query with rule-id', - note: '# test documentation', + note: '# this is some markdown documentation', references: ['www.test.co'], - risk_score: 1, + risk_score: 21, severity: 'low', tags: ['tag1', 'tag2'], threat: [ @@ -272,7 +297,7 @@ describe('helpers', () => { test('returns formatted object without note if note is empty string', () => { const mockStepData = { - ...mockAboutStepRule, + ...mockData, note: '', }; const result: AboutStepRuleJson = formatAboutStepData(mockStepData); @@ -281,7 +306,7 @@ describe('helpers', () => { false_positives: ['test'], name: 'Query with rule-id', references: ['www.test.co'], - risk_score: 1, + risk_score: 21, severity: 'low', tags: ['tag1', 'tag2'], threat: [ @@ -310,7 +335,7 @@ describe('helpers', () => { test('returns formatted object without timeline_id and timeline_title if timeline.id is null', () => { const mockStepData = { - ...mockAboutStepRule, + ...mockData, }; delete mockStepData.timeline.id; const result: AboutStepRuleJson = formatAboutStepData(mockStepData); @@ -318,9 +343,9 @@ describe('helpers', () => { description: '24/7', false_positives: ['test'], name: 'Query with rule-id', - note: '# test documentation', + note: '# this is some markdown documentation', references: ['www.test.co'], - risk_score: 1, + risk_score: 21, severity: 'low', tags: ['tag1', 'tag2'], threat: [ @@ -348,9 +373,9 @@ describe('helpers', () => { // Note: is this desired behavior? Should we also check for empty string? test('returns formatted object with timeline_id and timeline_title if timeline.id is ``', () => { const mockStepData = { - ...mockAboutStepRule, + ...mockData, timeline: { - ...mockAboutStepRule.timeline, + ...mockData.timeline, id: '', }, }; @@ -359,9 +384,9 @@ describe('helpers', () => { description: '24/7', false_positives: ['test'], name: 'Query with rule-id', - note: '# test documentation', + note: '# this is some markdown documentation', references: ['www.test.co'], - risk_score: 1, + risk_score: 21, severity: 'low', tags: ['tag1', 'tag2'], threat: [ @@ -390,9 +415,9 @@ describe('helpers', () => { test('returns formatted object without timeline_id and timeline_title if timeline.title is null', () => { const mockStepData = { - ...mockAboutStepRule, + ...mockData, timeline: { - ...mockAboutStepRule.timeline, + ...mockData.timeline, id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', }, }; @@ -402,9 +427,9 @@ describe('helpers', () => { description: '24/7', false_positives: ['test'], name: 'Query with rule-id', - note: '# test documentation', + note: '# this is some markdown documentation', references: ['www.test.co'], - risk_score: 1, + risk_score: 21, severity: 'low', tags: ['tag1', 'tag2'], threat: [ @@ -432,7 +457,7 @@ describe('helpers', () => { // Note: is this desired behavior? Should we also check for empty string? test('returns formatted object with timeline_id and timeline_title if timeline.title is ``', () => { const mockStepData = { - ...mockAboutStepRule, + ...mockData, timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: '', @@ -443,9 +468,9 @@ describe('helpers', () => { description: '24/7', false_positives: ['test'], name: 'Query with rule-id', - note: '# test documentation', + note: '# this is some markdown documentation', references: ['www.test.co'], - risk_score: 1, + risk_score: 21, severity: 'low', tags: ['tag1', 'tag2'], threat: [ @@ -464,7 +489,7 @@ describe('helpers', () => { test('returns formatted object with threats filtered out where tactic.name is `none`', () => { const mockStepData = { - ...mockAboutStepRule, + ...mockData, threat: [ { framework: 'mockFramework', @@ -503,9 +528,9 @@ describe('helpers', () => { description: '24/7', false_positives: ['test'], name: 'Query with rule-id', - note: '# test documentation', + note: '# this is some markdown documentation', references: ['www.test.co'], - risk_score: 1, + risk_score: 21, severity: 'low', tags: ['tag1', 'tag2'], threat: [ @@ -515,6 +540,8 @@ describe('helpers', () => { technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], }, ], + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', }; expect(result).toEqual(expected); @@ -522,50 +549,43 @@ describe('helpers', () => { }); describe('formatRule', () => { + let mockAbout: AboutStepRule; + let mockDefine: DefineStepRule; + let mockSchedule: ScheduleStepRule; + + beforeEach(() => { + mockAbout = mockAboutStepRule(); + mockDefine = mockDefineStepRule(); + mockSchedule = mockScheduleStepRule(); + }); + test('returns NewRule with type of saved_query when saved_id exists', () => { - const result: NewRule = formatRule( - mockDefineStepRule, - mockAboutStepRule, - mockScheduleStepRule - ); + const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule); expect(result.type).toEqual('saved_query'); }); test('returns NewRule with type of query when saved_id does not exist', () => { const mockDefineStepRuleWithoutSavedId = { - ...mockDefineStepRule, + ...mockDefine, queryBar: { - ...mockDefineStepRule.queryBar, + ...mockDefine.queryBar, saved_id: '', }, }; - const result: NewRule = formatRule( - mockDefineStepRuleWithoutSavedId, - mockAboutStepRule, - mockScheduleStepRule - ); + const result: NewRule = formatRule(mockDefineStepRuleWithoutSavedId, mockAbout, mockSchedule); expect(result.type).toEqual('query'); }); test('returns NewRule with id set to ruleId if ruleId exists', () => { - const result: NewRule = formatRule( - mockDefineStepRule, - mockAboutStepRule, - mockScheduleStepRule, - 'query-with-rule-id' - ); + const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, 'query-with-rule-id'); expect(result.id).toEqual('query-with-rule-id'); }); test('returns NewRule without id if ruleId does not exist', () => { - const result: NewRule = formatRule( - mockDefineStepRule, - mockAboutStepRule, - mockScheduleStepRule - ); + const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule); expect(result.id).toBeUndefined(); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 18c7c7abffd7ab..0c365f8ca04e58 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -38,13 +38,13 @@ import { } from '../../../../containers/source'; import { SpyRoute } from '../../../../utils/route/spy_routes'; +import { StepAboutRuleToggleDetails } from '../components/step_about_rule_details/'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import { SignalsHistogramPanel } from '../../components/signals_histogram_panel'; import { SignalsTable } from '../../components/signals'; import { useUserInfo } from '../../components/user_info'; import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; import { useSignalInfo } from '../../components/signals_info'; -import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { buildSignalsRuleIdFilter } from '../../components/signals/default_config'; @@ -53,7 +53,7 @@ import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../components/read_only_callout'; import { RuleSwitch } from '../components/rule_switch'; import { StepPanel } from '../components/step_panel'; -import { getStepsData, redirectToDetections } from '../helpers'; +import { getStepsDataDetails, redirectToDetections } from '../helpers'; import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { GlobalTime } from '../../../../containers/global_time'; @@ -105,13 +105,15 @@ const RuleDetailsPageComponent: FC = ({ // This is used to re-trigger api rule status when user de/activate rule const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); - const { aboutRuleData, defineRuleData, scheduleRuleData } = + const { aboutRuleData, aboutRuleDataDetails, defineRuleData, scheduleRuleData } = rule != null - ? getStepsData({ - rule, - detailsView: true, - }) - : { aboutRuleData: null, defineRuleData: null, scheduleRuleData: null }; + ? getStepsDataDetails(rule) + : { + aboutRuleData: null, + aboutRuleDataDetails: null, + defineRuleData: null, + scheduleRuleData: null, + }; const [lastSignals] = useSignalInfo({ ruleId }); const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; @@ -291,16 +293,23 @@ const RuleDetailsPageComponent: FC = ({
    {ruleError} - {tabs} - {ruleDetailTab === RuleDetailTabs.signals && ( - <> - + + + + + + + {defineRuleData != null && ( = ({ )} - - - - {aboutRuleData != null && ( - - )} - - - + {scheduleRuleData != null && ( = ({ - + + + + {tabs} + + {ruleDetailTab === RuleDetailTabs.signals && ( + <> { expect(result.aboutRuleData?.timeline.title).toBeNull(); }); - test('returns name as empty string if detailsView is true', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { const result: GetStepsData = getStepsData({ rule: mockRuleWithEverything('test-id'), detailsView: true, }); expect(result.aboutRuleData?.name).toEqual(''); + expect(result.aboutRuleData?.description).toEqual(''); + expect(result.aboutRuleData?.note).toEqual(''); }); test('returns note as empty string if property does not exist on rule', () => { @@ -151,7 +153,6 @@ describe('rule helpers', () => { delete mockedRule.note; const result: GetStepsData = getStepsData({ rule: mockedRule, - detailsView: true, }); expect(result.aboutRuleData?.note).toEqual(''); @@ -217,4 +218,85 @@ describe('rule helpers', () => { }); }); }); + + describe('getStepsDataDetails', () => { + test('returns object with about, about details, define, and schedule step properties formatted', () => { + const result = getStepsDataDetails(mockRuleWithEverything('test-id')); + const defineRuleStepData = { + isNew: false, + index: ['auditbeat-*'], + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', + }, + }; + const aboutRuleStepData = { + description: '', + falsePositives: ['test'], + isNew: false, + name: '', + note: '', + references: ['www.test.co'], + riskScore: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, + }; + const aboutRuleDataDetails = { + note: '# this is some markdown documentation', + description: '24/7', + }; + const scheduleRuleStepData = { enabled: true, from: '0s', interval: '5m', isNew: false }; + + expect(result.defineRuleData).toEqual(defineRuleStepData); + expect(result.aboutRuleData).toEqual(aboutRuleStepData); + expect(result.aboutRuleDataDetails).toEqual(aboutRuleDataDetails); + expect(result.scheduleRuleData).toEqual(scheduleRuleStepData); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 9368f08d63eaa4..9330a3e1fa02b0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -12,13 +12,25 @@ import { useLocation } from 'react-router-dom'; import { Filter } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; import { FormData, FormHook, FormSchema } from '../../../shared_imports'; -import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; +import { + AboutStepRule, + AboutStepRuleDetails, + DefineStepRule, + IMitreEnterpriseAttack, + ScheduleStepRule, +} from './types'; export interface GetStepsData { aboutRuleData: AboutStepRule | null; defineRuleData: DefineStepRule | null; scheduleRuleData: ScheduleStepRule | null; } +export interface GetStepsDataDetails { + aboutRuleData: AboutStepRule | null; + aboutRuleDataDetails: AboutStepRuleDetails; + defineRuleData: DefineStepRule | null; + scheduleRuleData: ScheduleStepRule | null; +} export const getStepsData = ({ rule, @@ -44,7 +56,13 @@ export const getStepsData = ({ ? { isNew: false, ...pick(['description', 'name', 'references', 'severity', 'tags', 'threat'], rule), - ...(detailsView ? { name: '' } : {}), + ...(detailsView + ? { + name: '', + description: '', + note: '', + } + : { note: rule.note ?? '' }), threat: rule.threat as IMitreEnterpriseAttack[], falsePositives: rule.false_positives, riskScore: rule.risk_score, @@ -52,7 +70,6 @@ export const getStepsData = ({ id: rule.timeline_id ?? null, title: rule.timeline_title ?? null, }, - note: rule.note ?? '', } : null; @@ -80,6 +97,24 @@ export const getStepsData = ({ return { aboutRuleData, defineRuleData, scheduleRuleData }; }; +export const getStepsDataDetails = (rule: Rule): GetStepsDataDetails => { + const { defineRuleData, aboutRuleData, scheduleRuleData } = getStepsData({ + rule, + detailsView: true, + }); + const modifiedAboutStepRuleData = { + note: rule.note ?? '', + description: rule.description ?? '', + }; + + return { + aboutRuleData, + aboutRuleDataDetails: modifiedAboutStepRuleData, + defineRuleData, + scheduleRuleData, + }; +}; + export const useQuery = () => new URLSearchParams(useLocation().search); export type PrePackagedRuleStatus = diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index e3e4eca0fd7aee..aa50626a1231af 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -61,6 +61,11 @@ export interface AboutStepRule extends StepRuleData { note: string; } +export interface AboutStepRuleDetails { + note: string; + description: string; +} + export interface DefineStepRule extends StepRuleData { index: string[]; queryBar: FieldValueQueryBar; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts index dd4acaeaf5a028..39277b3d3c77ee 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts @@ -19,7 +19,7 @@ export const TOTAL_SIGNAL = i18n.translate('xpack.siem.detectionEngine.totalSign }); export const SIGNAL = i18n.translate('xpack.siem.detectionEngine.signalTitle', { - defaultMessage: 'Signals (SIEM Detections)', + defaultMessage: 'Detected signals', }); export const ALERT = i18n.translate('xpack.siem.detectionEngine.alertTitle', { From 1662e7020c4d0888a17bc3e666163a1056c2eacd Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Fri, 13 Mar 2020 10:06:39 -0400 Subject: [PATCH 3/9] updated snapshots, re-ran integration tests --- .../__snapshots__/index.test.tsx.snap | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap index fddb315e3e44d0..aee2961b69a2a4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap @@ -80,6 +80,33 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against "description": + + + + + + + + + + + + , "title": "MITRE ATT&CK™", @@ -199,6 +226,33 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against "description": + + + + + + + + + + + + , "title": "MITRE ATT&CK™", @@ -319,6 +373,33 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against "description": + + + + + + + + + + + + , "title": "MITRE ATT&CK™", From 82c71c3795d3dd95b41ab32d518847159ee07da0 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Sat, 14 Mar 2020 12:59:39 -0400 Subject: [PATCH 4/9] updated details panel to remain constant heights on toggle and add scroll to description. tests updated. --- .../siem/cypress/screens/rule_details.ts | 4 +- .../step_about_rule_details/index.test.tsx | 20 ++- .../step_about_rule_details/index.tsx | 132 +++++++++++------- 3 files changed, 94 insertions(+), 62 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts index 46da52cd0ddd8a..b41d32f9a5f57a 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts @@ -25,10 +25,10 @@ export const ABOUT_TIMELINE = 3; export const DEFINITION_CUSTOM_QUERY = 1; export const DEFINITION_DESCRIPTION = - '[data-test-subj="definition"] .euiDescriptionList__description'; + '[data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; export const DEFINITION_INDEX_PATTERNS = - '[data-test-subj="definition"] .euiDescriptionList__description .euiBadge__text'; + '[data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description .euiBadge__text'; export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx index 996482588aaaf8..4ff81c9f4f6e59 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx @@ -87,13 +87,9 @@ describe('StepAboutRuleToggleDetails', () => { /> ); - expect(wrapper.find(EuiButtonGroup).exists()).toBeFalsy(); - expect( - wrapper - .find('EuiText[data-test-subj="stepAboutRuleDetailsToggleDescriptionText"] p') - .at(0) - .text() - ).toEqual(mockAboutStepWithoutNote.description); + expect(wrapper.find('[data-test-subj="stepAboutDetailsToggle"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="stepAboutDetailsNoteContent"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="stepAboutDetailsContent"]').exists()).toBeTruthy(); }); test('it does not render description as part of the description list', () => { @@ -124,7 +120,7 @@ describe('StepAboutRuleToggleDetails', () => { }); describe('note value does exist', () => { - test('it renders toggle buttons, defaulted to `about`', () => { + test('it renders toggle buttons, defaulted to `details`', () => { const wrapper = mount( { expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy(); expect( wrapper - .find('EuiButtonToggle[id="about"]') + .find('EuiButtonToggle[id="details"]') .at(0) .prop('isSelected') ).toBeTruthy(); @@ -153,7 +149,7 @@ describe('StepAboutRuleToggleDetails', () => { ).toBeFalsy(); }); - test('it allows users to toggle between `about` and `note`', () => { + test('it allows users to toggle between `details` and `note`', () => { const wrapper = mount( { ); - expect(wrapper.find('EuiButtonGroup[idSelected="about"]').exists()).toBeTruthy(); + expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeTruthy(); expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy(); wrapper @@ -175,7 +171,7 @@ describe('StepAboutRuleToggleDetails', () => { .at(0) .simulate('change', { target: { value: 'notes' } }); - expect(wrapper.find('EuiButtonGroup[idSelected="about"]').exists()).toBeFalsy(); + expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeFalsy(); expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx index ca6a1ca5063e31..ccad1c01c4681a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx @@ -12,6 +12,8 @@ import { EuiSpacer, EuiFlexItem, EuiText, + EuiFlexGroup, + EuiResizeObserver, } from '@elastic/eui'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; @@ -23,9 +25,30 @@ import { AboutStepRule, AboutStepRuleDetails } from '../../types'; import * as i18n from './translations'; import { StepAboutRule } from '../step_about_rule/'; +const MyPanel = styled(EuiPanel)` + position: relative; +`; + +const FlexGroupFullHeight = styled(EuiFlexGroup)` + height: 100%; +`; + +const VerticalOverflowContainer = styled.div((props: { maxHeight: number }) => ({ + 'max-height': `${props.maxHeight ?? '200'}px`, + 'overflow-y': 'hidden', +})); + +const VerticalOverflowContent = styled.div((props: { maxHeight: number }) => ({ + 'max-height': `${props.maxHeight}px`, +})); + +const AboutContent = styled.div` + height: 100%; +`; + const toggleOptions: EuiButtonGroupOption[] = [ { - id: 'about', + id: 'details', label: i18n.ABOUT_PANEL_DETAILS_TAB, }, { @@ -40,25 +63,17 @@ interface StepPanelProps { loading: boolean; } -const MyPanel = styled(EuiPanel)` - position: relative; -`; - -const AboutDescriptionContainer = styled(EuiFlexItem)` - max-height: 550px; - overflow-y: hidden; -`; - -const AboutDescriptionScrollContainer = styled.div` - overflow-x: hidden; -`; - const StepAboutRuleToggleDetailsComponent: React.FC = ({ stepData, stepDataDetails, loading, }) => { - const [selectedToggleOption, setToggleOption] = useState('about'); + const [selectedToggleOption, setToggleOption] = useState('details'); + const [aboutPanelHeight, setAboutPanelHeight] = useState(); + + const onResize = (e: { height: number; width: number }) => { + setAboutPanelHeight(e.height); + }; return ( @@ -69,40 +84,61 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ )} {stepData != null && stepDataDetails != null && ( - <> - - {!isEmpty(stepDataDetails.note) && ( - { - setToggleOption(val); - }} - /> - )} - - - - {selectedToggleOption === 'about' ? ( - <> - - -

    {stepDataDetails.description}

    -
    - - - - ) : ( - + + + + {!isEmpty(stepDataDetails.note) && ( + { + setToggleOption(val); + }} + data-test-subj="stepAboutDetailsToggle" + /> )} -
    -
    - + +
    + + {selectedToggleOption === 'details' ? ( + + {resizeRef => ( + + + + + {stepDataDetails.description} + + + + + + + )} + + ) : ( + + + + + + )} + + )} ); From 58a893c0947fed7c4810b03483ef957dafcf57d7 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Sat, 14 Mar 2020 23:09:20 -0400 Subject: [PATCH 5/9] removed empty dead tests and comments, updated some unit tests, ran unit and integration tests locally --- .../__snapshots__/index.test.tsx.snap | 452 ++++++++++++++++++ .../description_step/helpers.test.tsx | 12 +- .../components/description_step/helpers.tsx | 2 +- .../description_step/index.test.tsx | 12 +- .../components/step_about_rule/index.test.tsx | 18 +- .../step_about_rule_details/index.test.tsx | 36 +- .../rules/create/helpers.test.ts | 20 +- .../detection_engine/rules/helpers.test.tsx | 95 ++-- .../pages/detection_engine/rules/helpers.tsx | 13 +- 9 files changed, 545 insertions(+), 115 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap index aee2961b69a2a4..6b26a58e3a387b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap @@ -1,5 +1,457 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "multi" 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 21, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + ] + } + /> + + + +
      +
    • + + www.test.co + +
    • +
    + , + "title": "Reference URLs", + }, + Object { + "description": +
      +
    • + test +
    • +
    +
    , + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
    + # this is some markdown documentation +
    +
    , + "title": "Investigation notes", + }, + ] + } + /> +
    +
    +`; + +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "single" 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 21, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + Object { + "description": +
      +
    • + + www.test.co + +
    • +
    +
    , + "title": "Reference URLs", + }, + Object { + "description": +
      +
    • + test +
    • +
    +
    , + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
    + # this is some markdown documentation +
    +
    , + "title": "Investigation notes", + }, + ] + } + /> +
    +
    +`; + +exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "singleSplit 1`] = ` + + + , + "title": "Severity", + }, + Object { + "description": 21, + "title": "Risk score", + }, + Object { + "description": "Titled timeline", + "title": "Timeline template", + }, + Object { + "description": +
      +
    • + + www.test.co + +
    • +
    +
    , + "title": "Reference URLs", + }, + Object { + "description": +
      +
    • + test +
    • +
    +
    , + "title": "False positive examples", + }, + Object { + "description": + + + + + + + + + + + + + + , + "title": "MITRE ATT&CK™", + }, + Object { + "description": + + + tag1 + + + + + tag2 + + + , + "title": "Tags", + }, + Object { + "description": +
    + # this is some markdown documentation +
    +
    , + "title": "Investigation notes", + }, + ] + } + type="column" + /> +
    +
    +`; + exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is \`multi\` 1`] = ` { expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); }); - test('returns expected array of ListItems when `query.query` exists', () => { + test('returns expected array of ListItems when "query.query" exists', () => { const mockQueryBarWithQuery = { ...mockQueryBar, filters: [], @@ -174,7 +174,7 @@ describe('helpers', () => { expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query.query} ); }); - test('returns expected array of ListItems when `savedId` exists', () => { + test('returns expected array of ListItems when "savedId" exists', () => { const mockQueryBarWithSavedId = { ...mockQueryBar, query: { @@ -201,7 +201,6 @@ describe('helpers', () => { expect(result).toHaveLength(0); }); - // Not sure that this is the desired functionality, but just added tests per existing logic test('returns empty tactic link if no corresponding tactic id found', () => { const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', @@ -221,7 +220,6 @@ describe('helpers', () => { ); }); - // Not sure that this is the desired functionality, but just added tests per existing logic test('returns empty technique link if no corresponding technique id found', () => { const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', @@ -291,7 +289,7 @@ describe('helpers', () => { }); describe('buildUnorderedListArrayDescription', () => { - test('returns empty array if `values` is empty array', () => { + test('returns empty array if "values" is empty array', () => { const result: ListItems[] = buildUnorderedListArrayDescription( 'Test label', 'falsePositives', @@ -314,7 +312,7 @@ describe('helpers', () => { }); describe('buildStringArrayDescription', () => { - test('returns empty array if `values` is empty array', () => { + test('returns empty array if "values" is empty array', () => { const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); expect(result).toHaveLength(0); }); @@ -354,7 +352,7 @@ describe('helpers', () => { }); describe('buildUrlsDescription', () => { - test('returns empty array if `values` is empty array', () => { + test('returns empty array if "values" is empty array', () => { const result: ListItems[] = buildUrlsDescription('Test label', []); expect(result).toHaveLength(0); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index 2fa14a9e5c7e9a..410ff00a9f6969 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -173,7 +173,7 @@ export const buildUnorderedListArrayDescription = ( description: (
      - {values.map((val: string) => + {values.map(val => isEmpty(val) ? null : (
    • {val} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx index ba23d27c6c8c58..2c6f47fd27c443 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx @@ -54,7 +54,7 @@ describe('description_step', () => { }); describe('StepRuleDescriptionComponent', () => { - test('renders correctly against snapshot when columns is `multi`', () => { + test('renders correctly against snapshot when columns is "multi"', () => { const wrapper = shallow( ); @@ -62,7 +62,7 @@ describe('description_step', () => { expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); }); - test('renders correctly against snapshot when columns is `single`', () => { + test('renders correctly against snapshot when columns is "single"', () => { const wrapper = shallow( ); @@ -70,7 +70,7 @@ describe('description_step', () => { expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); }); - test('renders correctly against snapshot when columns is `singleSplit', () => { + test('renders correctly against snapshot when columns is "singleSplit', () => { const wrapper = shallow( ); @@ -292,7 +292,7 @@ describe('description_step', () => { expect(result[0].description).toEqual('24/7'); }); - test('returns empty array when `value` is a non-existant property in `field`', () => { + test('returns empty array when "value" is a non-existant property in "field"', () => { const result: ListItems[] = getDescriptionItem( 'jibberjabber', 'JibberJabber label', @@ -341,7 +341,7 @@ describe('description_step', () => { expect(React.isValidElement(result[0].description)).toBeTruthy(); }); - test('filters out threats with tactic.name of `none`', () => { + test('filters out threats with tactic.name of "none"', () => { const mockStep = { ...mockAboutStep, threat: [ @@ -462,7 +462,7 @@ describe('description_step', () => { }); describe('note', () => { - test('returns default `note` description', () => { + test('returns default "note" description', () => { const result: ListItems[] = getDescriptionItem( 'note', 'Investigation notes', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx index 4654a2413188ce..0ed479e2351517 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx @@ -16,9 +16,7 @@ import { stepAboutDefaultValue } from './default_value'; const theme = () => ({ eui: euiDarkVars, darkMode: true }); describe('StepAboutRuleComponent', () => { - test('it renders StepRuleDescription if isReadOnlyView is true and `name` property exists', () => { - // see mockAboutStepRule for name property - // Note: is the name check for old rules? It's required so no rules should ever not include it + test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => { const wrapper = shallow( { expect(wrapper.find(StepRuleDescription).exists()).toBeTruthy(); }); - test('it prevents user from clicking continue if no `description` defined', () => { + test('it prevents user from clicking continue if no "description" defined', () => { const wrapper = mount( { ).toBeTruthy(); }); - test('it prevents user from clicking continue if no `name` defined', () => { + test('it prevents user from clicking continue if no "name" defined', () => { const wrapper = mount( { ).toBeTruthy(); }); - test('it allows user to click continue if `name` and `description` are defined', () => { + test('it allows user to click continue if "name" and "description" are defined', () => { const wrapper = mount( { const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); nextButton.simulate('click'); }); - - describe('advanced settings', () => { - test('it renders advanced settings collapsed initially', () => {}); - - test('it expands to show advanced fields', () => {}); - }); - - describe('submission', () => {}); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx index 4ff81c9f4f6e59..4a4e96ec749026 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx @@ -31,7 +31,7 @@ describe('StepAboutRuleToggleDetails', () => { mockRule = mockAboutStepRule(); }); - test('it renders loading component when `loading` is true', () => { + test('it renders loading component when "loading" is true', () => { const wrapper = shallow( { expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); }); - describe('note value does NOT exist', () => { + describe('note value is empty string', () => { test('it does not render toggle buttons', () => { const mockAboutStepWithoutNote = { ...mockRule, @@ -91,36 +91,10 @@ describe('StepAboutRuleToggleDetails', () => { expect(wrapper.find('[data-test-subj="stepAboutDetailsNoteContent"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="stepAboutDetailsContent"]').exists()).toBeTruthy(); }); - - test('it does not render description as part of the description list', () => { - const mockAboutStepWithoutNote = { - ...mockRule, - note: '', - }; - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="stepAboutRuleDetailsToggleDescriptionText"]') - .at(0) - .text() - ).toEqual(mockAboutStepWithoutNote.description); - }); }); describe('note value does exist', () => { - test('it renders toggle buttons, defaulted to `details`', () => { + test('it renders toggle buttons, defaulted to "details"', () => { const wrapper = mount( { ).toBeFalsy(); }); - test('it allows users to toggle between `details` and `note`', () => { + test('it allows users to toggle between "details" and "note"', () => { const wrapper = mount( { expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); }); - test('it displays notes markdown when user toggles to `notes`', () => { + test('it displays notes markdown when user toggles to "notes"', () => { const wrapper = mount( { expect(result).toEqual({ unit: 'm', value: 5 }); }); - test('returns timeObj with value of 0 and unit of `` if random string passed in', () => { + test('returns timeObj with value of 0 and unit of "" if random string passed in', () => { const result = getTimeTypeValue('random'); expect(result).toEqual({ unit: '', value: 0 }); @@ -134,7 +134,7 @@ describe('helpers', () => { expect(result).toEqual(expected); }); - test('returns formatted object with `to` as `now` if `to` not supplied', () => { + test('returns formatted object with "to" as "now" if "to" not supplied', () => { const mockStepData = { ...mockData, }; @@ -153,7 +153,7 @@ describe('helpers', () => { expect(result).toEqual(expected); }); - test('returns formatted object with `to` as `now` if `to` random string', () => { + test('returns formatted object with "to" as "now" if "to" random string', () => { const mockStepData = { ...mockData, to: 'random', @@ -172,8 +172,7 @@ describe('helpers', () => { expect(result).toEqual(expected); }); - // Note: do we want from to default to anything if it somehow ends up being unparsable string? - test('returns formatted object if `from` random string', () => { + test('returns formatted object if "from" random string', () => { const mockStepData = { ...mockData, from: 'random', @@ -192,8 +191,7 @@ describe('helpers', () => { expect(result).toEqual(expected); }); - // Note: do we want interval to default to anything if it somehow ends up being unparsable string? - test('returns formatted object if `interval` random string', () => { + test('returns formatted object if "interval" random string', () => { const mockStepData = { ...mockData, interval: 'random', @@ -370,8 +368,7 @@ describe('helpers', () => { expect(result).toEqual(expected); }); - // Note: is this desired behavior? Should we also check for empty string? - test('returns formatted object with timeline_id and timeline_title if timeline.id is ``', () => { + test('returns formatted object with timeline_id and timeline_title if timeline.id is "', () => { const mockStepData = { ...mockData, timeline: { @@ -454,8 +451,7 @@ describe('helpers', () => { expect(result).toEqual(expected); }); - // Note: is this desired behavior? Should we also check for empty string? - test('returns formatted object with timeline_id and timeline_title if timeline.title is ``', () => { + test('returns formatted object with timeline_id and timeline_title if timeline.title is "', () => { const mockStepData = { ...mockData, timeline: { @@ -487,7 +483,7 @@ describe('helpers', () => { expect(result).toEqual(expected); }); - test('returns formatted object with threats filtered out where tactic.name is `none`', () => { + test('returns formatted object with threats filtered out where tactic.name is "none"', () => { const mockStepData = { ...mockData, threat: [ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx index b52546d5853c0c..4b5998d86d2d39 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetStepsData, getStepsData, getStepsDataDetails } from './helpers'; +import { GetStepsData, GetStepsDataDetails, getStepsData, getStepsDataDetails } from './helpers'; import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; describe('rule helpers', () => { describe('getStepsData', () => { test('returns object with about, define, and schedule step properties formatted', () => { - const result = getStepsData({ rule: mockRuleWithEverything('test-id') }); + const { defineRuleData, aboutRuleData, scheduleRuleData }: GetStepsData = getStepsData({ + rule: mockRuleWithEverything('test-id'), + }); const defineRuleStepData = { isNew: false, index: ['auditbeat-*'], @@ -79,14 +81,14 @@ describe('rule helpers', () => { }; const scheduleRuleStepData = { enabled: true, from: '0s', interval: '5m', isNew: false }; - expect(result.defineRuleData).toEqual(defineRuleStepData); - expect(result.aboutRuleData).toEqual(aboutRuleStepData); - expect(result.scheduleRuleData).toEqual(scheduleRuleStepData); + expect(defineRuleData).toEqual(defineRuleStepData); + expect(aboutRuleData).toEqual(aboutRuleStepData); + expect(scheduleRuleData).toEqual(scheduleRuleStepData); }); describe('defineStepRule', () => { test('returns with saved_id if value exists on rule', () => { - const result: GetStepsData = getStepsData({ rule: mockRule('test-id') }); + const { defineRuleData }: GetStepsData = getStepsData({ rule: mockRule('test-id') }); const expected = { isNew: false, index: ['auditbeat-*'], @@ -100,7 +102,7 @@ describe('rule helpers', () => { }, }; - expect(result.defineRuleData).toEqual(expected); + expect(defineRuleData).toEqual(expected); }); test('returns with saved_id of null if value does not exist on rule', () => { @@ -108,7 +110,7 @@ describe('rule helpers', () => { ...mockRule('test-id'), }; delete mockedRule.saved_id; - const result: GetStepsData = getStepsData({ rule: mockedRule }); + const { defineRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); const expected = { isNew: false, index: ['auditbeat-*'], @@ -122,7 +124,7 @@ describe('rule helpers', () => { }, }; - expect(result.defineRuleData).toEqual(expected); + expect(defineRuleData).toEqual(expected); }); }); @@ -131,31 +133,31 @@ describe('rule helpers', () => { const mockedRule = mockRuleWithEverything('test-id'); delete mockedRule.timeline_id; delete mockedRule.timeline_title; - const result: GetStepsData = getStepsData({ rule: mockedRule }); + const { aboutRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); - expect(result.aboutRuleData?.timeline.id).toBeNull(); - expect(result.aboutRuleData?.timeline.title).toBeNull(); + expect(aboutRuleData?.timeline.id).toBeNull(); + expect(aboutRuleData?.timeline.title).toBeNull(); }); test('returns name, description, and note as empty string if detailsView is true', () => { - const result: GetStepsData = getStepsData({ + const { aboutRuleData }: GetStepsData = getStepsData({ rule: mockRuleWithEverything('test-id'), detailsView: true, }); - expect(result.aboutRuleData?.name).toEqual(''); - expect(result.aboutRuleData?.description).toEqual(''); - expect(result.aboutRuleData?.note).toEqual(''); + expect(aboutRuleData?.name).toEqual(''); + expect(aboutRuleData?.description).toEqual(''); + expect(aboutRuleData?.note).toEqual(''); }); test('returns note as empty string if property does not exist on rule', () => { const mockedRule = mockRuleWithEverything('test-id'); delete mockedRule.note; - const result: GetStepsData = getStepsData({ + const { aboutRuleData }: GetStepsData = getStepsData({ rule: mockedRule, }); - expect(result.aboutRuleData?.note).toEqual(''); + expect(aboutRuleData?.note).toEqual(''); }); }); @@ -166,10 +168,10 @@ describe('rule helpers', () => { from: 'now-62s', interval: '1m', }; - const result = getStepsData({ rule: mockedRule }); + const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); - expect(result.scheduleRuleData?.from).toEqual('2s'); - expect(result.scheduleRuleData?.interval).toEqual('1m'); + expect(scheduleRuleData?.from).toEqual('2s'); + expect(scheduleRuleData?.interval).toEqual('1m'); }); test('returns from as minutes if from duration is less than an hour', () => { @@ -177,10 +179,10 @@ describe('rule helpers', () => { ...mockRule('test-id'), from: 'now-660s', }; - const result = getStepsData({ rule: mockedRule }); + const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); - expect(result.scheduleRuleData?.from).toEqual('6m'); - expect(result.scheduleRuleData?.interval).toEqual('5m'); + expect(scheduleRuleData?.from).toEqual('6m'); + expect(scheduleRuleData?.interval).toEqual('5m'); }); test('returns from as hours if from duration is more than 60 minutes', () => { @@ -189,21 +191,20 @@ describe('rule helpers', () => { from: 'now-7400s', interval: '5m', }; - const result = getStepsData({ rule: mockedRule }); + const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); - expect(result.scheduleRuleData?.from).toEqual('1h'); - expect(result.scheduleRuleData?.interval).toEqual('5m'); + expect(scheduleRuleData?.from).toEqual('1h'); + expect(scheduleRuleData?.interval).toEqual('5m'); }); - // Note: is this desired behavior? test('returns from as if from is not parsable as dateMath', () => { const mockedRule = { ...mockRule('test-id'), from: 'randomstring', }; - const result = getStepsData({ rule: mockedRule }); + const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); - expect(result.scheduleRuleData?.from).toEqual('NaNh'); + expect(scheduleRuleData?.from).toEqual('NaNh'); }); test('returns from as 5m if interval is not parsable as dateMath', () => { @@ -211,17 +212,22 @@ describe('rule helpers', () => { ...mockRule('test-id'), interval: 'randomstring', }; - const result = getStepsData({ rule: mockedRule }); + const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); - expect(result.scheduleRuleData?.from).toEqual('5m'); - expect(result.scheduleRuleData?.interval).toEqual('randomstring'); + expect(scheduleRuleData?.from).toEqual('5m'); + expect(scheduleRuleData?.interval).toEqual('randomstring'); }); }); }); describe('getStepsDataDetails', () => { test('returns object with about, about details, define, and schedule step properties formatted', () => { - const result = getStepsDataDetails(mockRuleWithEverything('test-id')); + const { + defineRuleData, + aboutRuleData, + aboutRuleDataDetails, + scheduleRuleData, + }: GetStepsDataDetails = getStepsDataDetails(mockRuleWithEverything('test-id')); const defineRuleStepData = { isNew: false, index: ['auditbeat-*'], @@ -287,16 +293,27 @@ describe('rule helpers', () => { title: 'Titled timeline', }, }; - const aboutRuleDataDetails = { + const aboutRuleDataDetailsData = { note: '# this is some markdown documentation', description: '24/7', }; const scheduleRuleStepData = { enabled: true, from: '0s', interval: '5m', isNew: false }; - expect(result.defineRuleData).toEqual(defineRuleStepData); - expect(result.aboutRuleData).toEqual(aboutRuleStepData); - expect(result.aboutRuleDataDetails).toEqual(aboutRuleDataDetails); - expect(result.scheduleRuleData).toEqual(scheduleRuleStepData); + expect(defineRuleData).toEqual(defineRuleStepData); + expect(aboutRuleData).toEqual(aboutRuleStepData); + expect(aboutRuleDataDetails).toEqual(aboutRuleDataDetailsData); + expect(scheduleRuleData).toEqual(scheduleRuleStepData); + }); + + test('returns aboutRuleDataDetails with empty string if "note" does not exist', () => { + const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; + const { aboutRuleDataDetails }: GetStepsDataDetails = getStepsDataDetails( + mockRuleWithoutNote + ); + + const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; + + expect(aboutRuleDataDetails).toEqual(aboutRuleDetailsData); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 9330a3e1fa02b0..a20a5139a173de 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -27,7 +27,7 @@ export interface GetStepsData { } export interface GetStepsDataDetails { aboutRuleData: AboutStepRule | null; - aboutRuleDataDetails: AboutStepRuleDetails; + aboutRuleDataDetails: AboutStepRuleDetails | null; defineRuleData: DefineStepRule | null; scheduleRuleData: ScheduleStepRule | null; } @@ -102,10 +102,13 @@ export const getStepsDataDetails = (rule: Rule): GetStepsDataDetails => { rule, detailsView: true, }); - const modifiedAboutStepRuleData = { - note: rule.note ?? '', - description: rule.description ?? '', - }; + const modifiedAboutStepRuleData: AboutStepRuleDetails | null = + rule != null + ? { + note: rule.note ?? '', + description: rule.description, + } + : null; return { aboutRuleData, From 88210f1c1a3bc0d263f4f31c3bcfdacd5808f6d0 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Sat, 14 Mar 2020 23:46:55 -0400 Subject: [PATCH 6/9] updated helper to the more concise version suggested by frank --- .../rules/components/description_step/helpers.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index 410ff00a9f6969..1aeff1d2e86ffe 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -33,8 +33,7 @@ const NoteDescriptionContainer = styled(EuiFlexItem)` overflow-y: hidden; `; -export const isNotEmptyArray = (values: string[]) => - !isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0; +export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); const EuiBadgeWrap = styled(EuiBadge)` .euiBadge__text { From 9ac25ca987bb7476f792bc16c9394805a883a2b7 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Mon, 16 Mar 2020 09:55:00 -0400 Subject: [PATCH 7/9] removed dead snapshots, still debugging cypress tests locally --- .../__snapshots__/index.test.tsx.snap | 452 ------------------ 1 file changed, 452 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap index 6b26a58e3a387b..4d416e70a096c6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap @@ -451,455 +451,3 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against `; - -exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is \`multi\` 1`] = ` - - - , - "title": "Severity", - }, - Object { - "description": 21, - "title": "Risk score", - }, - Object { - "description": "Titled timeline", - "title": "Timeline template", - }, - ] - } - /> - - - -
        -
      • - - www.test.co - -
      • -
      - , - "title": "Reference URLs", - }, - Object { - "description": -
        -
      • - test -
      • -
      -
      , - "title": "False positive examples", - }, - Object { - "description": - - - - - - - - - - - - - - , - "title": "MITRE ATT&CK™", - }, - Object { - "description": - - - tag1 - - - - - tag2 - - - , - "title": "Tags", - }, - Object { - "description": -
      - # this is some markdown documentation -
      -
      , - "title": "Investigation notes", - }, - ] - } - /> -
      -
      -`; - -exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is \`single\` 1`] = ` - - - , - "title": "Severity", - }, - Object { - "description": 21, - "title": "Risk score", - }, - Object { - "description": "Titled timeline", - "title": "Timeline template", - }, - Object { - "description": -
        -
      • - - www.test.co - -
      • -
      -
      , - "title": "Reference URLs", - }, - Object { - "description": -
        -
      • - test -
      • -
      -
      , - "title": "False positive examples", - }, - Object { - "description": - - - - - - - - - - - - - - , - "title": "MITRE ATT&CK™", - }, - Object { - "description": - - - tag1 - - - - - tag2 - - - , - "title": "Tags", - }, - Object { - "description": -
      - # this is some markdown documentation -
      -
      , - "title": "Investigation notes", - }, - ] - } - /> -
      -
      -`; - -exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is \`singleSplit 1`] = ` - - - , - "title": "Severity", - }, - Object { - "description": 21, - "title": "Risk score", - }, - Object { - "description": "Titled timeline", - "title": "Timeline template", - }, - Object { - "description": -
        -
      • - - www.test.co - -
      • -
      -
      , - "title": "Reference URLs", - }, - Object { - "description": -
        -
      • - test -
      • -
      -
      , - "title": "False positive examples", - }, - Object { - "description": - - - - - - - - - - - - - - , - "title": "MITRE ATT&CK™", - }, - Object { - "description": - - - tag1 - - - - - tag2 - - - , - "title": "Tags", - }, - Object { - "description": -
      - # this is some markdown documentation -
      -
      , - "title": "Investigation notes", - }, - ] - } - type="column" - /> -
      -
      -`; From 78e8878c66bc496eded6fbf945039ed225526006 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Mon, 16 Mar 2020 20:33:49 +0100 Subject: [PATCH 8/9] updates 'Creates and activates new rule' with the new layout changes --- .../signal_detection_rules.spec.ts | 108 +++++++++--------- .../siem/cypress/screens/rule_details.ts | 28 ++--- .../components/step_about_rule/index.tsx | 2 +- .../components/step_define_rule/index.tsx | 2 +- 4 files changed, 69 insertions(+), 71 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts index 8c384c90106657..ce73fe1b7c2a53 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -7,30 +7,30 @@ import { newRule } from '../objects/rule'; import { - ABOUT_DESCRIPTION, - ABOUT_EXPECTED_URLS, ABOUT_FALSE_POSITIVES, ABOUT_MITRE, ABOUT_RISK, - ABOUT_RULE_DESCRIPTION, ABOUT_SEVERITY, + ABOUT_STEP, ABOUT_TAGS, ABOUT_TIMELINE, + ABOUT_URLS, DEFINITION_CUSTOM_QUERY, - DEFINITION_DESCRIPTION, DEFINITION_INDEX_PATTERNS, + DEFINITION_STEP, RULE_NAME_HEADER, - SCHEDULE_DESCRIPTION, SCHEDULE_LOOPBACK, SCHEDULE_RUNS, + SCHEDULE_STEP, + ABOUT_RULE_DESCRIPTION, } from '../screens/rule_details'; import { CUSTOM_RULES_BTN, ELASTIC_RULES_BTN, RISK_SCORE, RULE_NAME, - RULES_TABLE, RULES_ROW, + RULES_TABLE, SEVERITY, } from '../screens/signal_detection_rules'; @@ -127,10 +127,25 @@ describe('Signal detection rules', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER) - .invoke('text') - .should('eql', `${newRule.name} Beta`); - + let expectedUrls = ''; + newRule.referenceUrls.forEach(url => { + expectedUrls = expectedUrls + url; + }); + let expectedFalsePositives = ''; + newRule.falsePositivesExamples.forEach(falsePositive => { + expectedFalsePositives = expectedFalsePositives + falsePositive; + }); + let expectedTags = ''; + newRule.tags.forEach(tag => { + expectedTags = expectedTags + tag; + }); + let expectedMitre = ''; + newRule.mitre.forEach(mitre => { + expectedMitre = expectedMitre + mitre.tactic; + mitre.techniques.forEach(technique => { + expectedMitre = expectedMitre + technique; + }); + }); const expectedIndexPatterns = [ 'apm-*-transaction*', 'auditbeat-*', @@ -139,77 +154,60 @@ describe('Signal detection rules', () => { 'packetbeat-*', 'winlogbeat-*', ]; - cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { - cy.wrap(patterns).each((pattern, index) => { - cy.wrap(pattern) - .invoke('text') - .should('eql', expectedIndexPatterns[index]); - }); - }); - cy.get(DEFINITION_DESCRIPTION) - .eq(DEFINITION_CUSTOM_QUERY) + + cy.get(RULE_NAME_HEADER) .invoke('text') - .should('eql', `${newRule.customQuery} `); - cy.get(ABOUT_DESCRIPTION) - .eq(ABOUT_RULE_DESCRIPTION) + .should('eql', `${newRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION) .invoke('text') .should('eql', newRule.description); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_SEVERITY) .invoke('text') .should('eql', newRule.severity); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_RISK) .invoke('text') .should('eql', newRule.riskScore); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_TIMELINE) .invoke('text') .should('eql', 'Default blank timeline'); - - let expectedUrls = ''; - newRule.referenceUrls.forEach(url => { - expectedUrls = expectedUrls + url; - }); - cy.get(ABOUT_DESCRIPTION) - .eq(ABOUT_EXPECTED_URLS) + cy.get(ABOUT_STEP) + .eq(ABOUT_URLS) .invoke('text') .should('eql', expectedUrls); - - let expectedFalsePositives = ''; - newRule.falsePositivesExamples.forEach(falsePositive => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_FALSE_POSITIVES) .invoke('text') .should('eql', expectedFalsePositives); - - let expectedMitre = ''; - newRule.mitre.forEach(mitre => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach(technique => { - expectedMitre = expectedMitre + technique; - }); - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_MITRE) .invoke('text') .should('eql', expectedMitre); - - let expectedTags = ''; - newRule.tags.forEach(tag => { - expectedTags = expectedTags + tag; - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_TAGS) .invoke('text') .should('eql', expectedTags); - cy.get(SCHEDULE_DESCRIPTION) + + cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { + cy.wrap(patterns).each((pattern, index) => { + cy.wrap(pattern) + .invoke('text') + .should('eql', expectedIndexPatterns[index]); + }); + }); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_CUSTOM_QUERY) + .invoke('text') + .should('eql', `${newRule.customQuery} `); + + cy.get(SCHEDULE_STEP) .eq(SCHEDULE_RUNS) .invoke('text') .should('eql', '5m'); - cy.get(SCHEDULE_DESCRIPTION) + cy.get(SCHEDULE_STEP) .eq(SCHEDULE_LOOPBACK) .invoke('text') .should('eql', '1m'); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts index b41d32f9a5f57a..6c16735ba5f24b 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts @@ -4,35 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ABOUT_DESCRIPTION = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; +export const ABOUT_FALSE_POSITIVES = 4; -export const ABOUT_EXPECTED_URLS = 4; +export const ABOUT_MITRE = 5; -export const ABOUT_FALSE_POSITIVES = 5; +export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; -export const ABOUT_MITRE = 6; +export const ABOUT_RISK = 1; -export const ABOUT_RULE_DESCRIPTION = 0; +export const ABOUT_SEVERITY = 0; -export const ABOUT_RISK = 2; +export const ABOUT_STEP = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; -export const ABOUT_SEVERITY = 1; +export const ABOUT_TAGS = 6; -export const ABOUT_TAGS = 7; +export const ABOUT_TIMELINE = 2; -export const ABOUT_TIMELINE = 3; +export const ABOUT_URLS = 3; export const DEFINITION_CUSTOM_QUERY = 1; -export const DEFINITION_DESCRIPTION = - '[data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; - export const DEFINITION_INDEX_PATTERNS = - '[data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description .euiBadge__text'; + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description .euiBadge__text'; + +export const DEFINITION_STEP = + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; -export const SCHEDULE_DESCRIPTION = '[data-test-subj="schedule"] .euiDescriptionList__description'; +export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description'; export const SCHEDULE_RUNS = 0; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index d7678f1975f009..bfb123f3f32042 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -127,7 +127,7 @@ const StepAboutRuleComponent: FC = ({ }, [form]); return isReadOnlyView && myStepData.name != null ? ( - + ) : ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 5064f9d3bae9e4..2327ac36a5906e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -155,7 +155,7 @@ const StepDefineRuleComponent: FC = ({ }, []); return isReadOnlyView && myStepData?.queryBar != null ? ( - + Date: Tue, 17 Mar 2020 10:49:31 -0400 Subject: [PATCH 9/9] added test files to circular dep check exception, updated some helpers per pr feedback --- .../run_check_circular_deps_cli.js | 10 + .../description_step/helpers.test.tsx | 8 +- .../components/description_step/helpers.tsx | 4 +- .../step_about_rule_details/index.tsx | 6 +- .../detection_engine/rules/details/index.tsx | 10 +- .../detection_engine/rules/helpers.test.tsx | 354 ++++++++---------- .../pages/detection_engine/rules/helpers.tsx | 171 +++++---- 7 files changed, 284 insertions(+), 279 deletions(-) diff --git a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js index 8ca61b2397d8b8..f3a97f5b9c9b6f 100644 --- a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js @@ -17,6 +17,16 @@ run( [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], { fileExtensions: ['ts', 'js', 'tsx'], + excludeRegExp: [ + 'test.ts$', + 'test.tsx$', + 'containers/detection_engine/rules/types.ts$', + 'core/public/chrome/chrome_service.tsx$', + 'src/core/server/types.ts$', + 'src/core/server/saved_objects/types.ts$', + 'src/core/public/overlays/banners/banners_service.tsx$', + 'src/core/public/saved_objects/saved_objects_client.ts$', + ], } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx index bc2185df4c8eed..56c9d6da156074 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx @@ -382,7 +382,7 @@ describe('helpers', () => { }); describe('buildNoteDescription', () => { - test('returns ListItem with passed in label and SeverityBadge component', () => { + test('returns ListItem with passed in label and note content', () => { const noteSample = 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; const result: ListItems[] = buildNoteDescription('Test label', noteSample); @@ -393,5 +393,11 @@ describe('helpers', () => { expect(noteElement.exists()).toBeTruthy(); expect(noteElement.text()).toEqual(noteSample); }); + + test('returns empty array if passed in note is empty string', () => { + const result: ListItems[] = buildNoteDescription('Test label', ''); + + expect(result).toHaveLength(0); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index 1aeff1d2e86ffe..bc454ecb1134a8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -233,7 +233,7 @@ export const buildUrlsDescription = (label: string, values: string[]): ListItems
        {values .filter(v => !isEmpty(v)) - .map((val: string, index: number) => ( + .map((val, index) => (
      • {val} @@ -250,7 +250,7 @@ export const buildUrlsDescription = (label: string, values: string[]): ListItems }; export const buildNoteDescription = (label: string, note: string): ListItems[] => { - if (note) { + if (note.trim() !== '') { return [ { title: label, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx index ccad1c01c4681a..c61566cb841e89 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx @@ -34,7 +34,7 @@ const FlexGroupFullHeight = styled(EuiFlexGroup)` `; const VerticalOverflowContainer = styled.div((props: { maxHeight: number }) => ({ - 'max-height': `${props.maxHeight ?? '200'}px`, + 'max-height': `${props.maxHeight}px`, 'overflow-y': 'hidden', })); @@ -69,7 +69,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ loading, }) => { const [selectedToggleOption, setToggleOption] = useState('details'); - const [aboutPanelHeight, setAboutPanelHeight] = useState(); + const [aboutPanelHeight, setAboutPanelHeight] = useState(0); const onResize = (e: { height: number; width: number }) => { setAboutPanelHeight(e.height); @@ -87,7 +87,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ - {!isEmpty(stepDataDetails.note) && ( + {!isEmpty(stepDataDetails.note) && stepDataDetails.note.trim() !== '' && ( = ({ // This is used to re-trigger api rule status when user de/activate rule const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); - const { aboutRuleData, aboutRuleDataDetails, defineRuleData, scheduleRuleData } = + const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = rule != null - ? getStepsDataDetails(rule) + ? getStepsData({ rule, detailsView: true }) : { aboutRuleData: null, - aboutRuleDataDetails: null, + modifiedAboutRuleDetailsData: null, defineRuleData: null, scheduleRuleData: null, }; @@ -299,7 +299,7 @@ const RuleDetailsPageComponent: FC = ({ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx index 4b5998d86d2d39..0c29bc31cdebc8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -4,14 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetStepsData, GetStepsDataDetails, getStepsData, getStepsDataDetails } from './helpers'; +import { + GetStepsData, + getDefineStepsData, + getScheduleStepsData, + getStepsData, + getAboutStepsData, + getHumanizedDuration, + getModifiedAboutDetailsData, + determineDetailsValue, +} from './helpers'; import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { Rule } from '../../../containers/detection_engine/rules'; +import { AboutStepRule, AboutStepRuleDetails, DefineStepRule, ScheduleStepRule } from './types'; describe('rule helpers', () => { describe('getStepsData', () => { test('returns object with about, define, and schedule step properties formatted', () => { - const { defineRuleData, aboutRuleData, scheduleRuleData }: GetStepsData = getStepsData({ + const { + defineRuleData, + modifiedAboutRuleDetailsData, + aboutRuleData, + scheduleRuleData, + }: GetStepsData = getStepsData({ rule: mockRuleWithEverything('test-id'), }); const defineRuleStepData = { @@ -80,240 +96,196 @@ describe('rule helpers', () => { }, }; const scheduleRuleStepData = { enabled: true, from: '0s', interval: '5m', isNew: false }; + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; expect(defineRuleData).toEqual(defineRuleStepData); expect(aboutRuleData).toEqual(aboutRuleStepData); expect(scheduleRuleData).toEqual(scheduleRuleStepData); + expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData); }); + }); - describe('defineStepRule', () => { - test('returns with saved_id if value exists on rule', () => { - const { defineRuleData }: GetStepsData = getStepsData({ rule: mockRule('test-id') }); - const expected = { - isNew: false, - index: ['auditbeat-*'], - queryBar: { - query: { - query: '', - language: 'kuery', - }, - filters: [], - saved_id: "Garrett's IP", - }, - }; - - expect(defineRuleData).toEqual(expected); - }); - - test('returns with saved_id of null if value does not exist on rule', () => { - const mockedRule = { - ...mockRule('test-id'), - }; - delete mockedRule.saved_id; - const { defineRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); - const expected = { - isNew: false, - index: ['auditbeat-*'], - queryBar: { - query: { - query: '', - language: 'kuery', - }, - filters: [], - saved_id: null, - }, - }; + describe('getAboutStepsData', () => { + test('returns timeline id and title of null if they do not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.timeline_id; + delete mockedRule.timeline_title; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); - expect(defineRuleData).toEqual(expected); - }); + expect(result.timeline.id).toBeNull(); + expect(result.timeline.title).toBeNull(); }); - describe('aboutRuleData', () => { - test('returns timeline id and title of null if they do not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.timeline_id; - delete mockedRule.timeline_title; - const { aboutRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: AboutStepRule = getAboutStepsData(mockRuleWithEverything('test-id'), true); - expect(aboutRuleData?.timeline.id).toBeNull(); - expect(aboutRuleData?.timeline.title).toBeNull(); - }); - - test('returns name, description, and note as empty string if detailsView is true', () => { - const { aboutRuleData }: GetStepsData = getStepsData({ - rule: mockRuleWithEverything('test-id'), - detailsView: true, - }); - - expect(aboutRuleData?.name).toEqual(''); - expect(aboutRuleData?.description).toEqual(''); - expect(aboutRuleData?.note).toEqual(''); - }); - - test('returns note as empty string if property does not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.note; - const { aboutRuleData }: GetStepsData = getStepsData({ - rule: mockedRule, - }); - - expect(aboutRuleData?.note).toEqual(''); - }); + expect(result.name).toEqual(''); + expect(result.description).toEqual(''); + expect(result.note).toEqual(''); }); - describe('scheduleRuleData', () => { - test('returns from as seconds if from duration is less than a minute', () => { - const mockedRule = { - ...mockRule('test-id'), - from: 'now-62s', - interval: '1m', - }; - const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); - - expect(scheduleRuleData?.from).toEqual('2s'); - expect(scheduleRuleData?.interval).toEqual('1m'); - }); - - test('returns from as minutes if from duration is less than an hour', () => { - const mockedRule = { - ...mockRule('test-id'), - from: 'now-660s', - }; - const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); - expect(scheduleRuleData?.from).toEqual('6m'); - expect(scheduleRuleData?.interval).toEqual('5m'); - }); + expect(result.note).toEqual(''); + }); + }); - test('returns from as hours if from duration is more than 60 minutes', () => { - const mockedRule = { - ...mockRule('test-id'), - from: 'now-7400s', - interval: '5m', - }; - const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); + describe('determineDetailsValue', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: Pick = determineDetailsValue( + mockRuleWithEverything('test-id'), + true + ); + const expected = { name: '', description: '', note: '' }; - expect(scheduleRuleData?.from).toEqual('1h'); - expect(scheduleRuleData?.interval).toEqual('5m'); - }); + expect(result).toEqual(expected); + }); - test('returns from as if from is not parsable as dateMath', () => { - const mockedRule = { - ...mockRule('test-id'), - from: 'randomstring', - }; - const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); + test('returns name, description, and note values if detailsView is false', () => { + const mockedRule = mockRuleWithEverything('test-id'); + const result: Pick = determineDetailsValue( + mockedRule, + false + ); + const expected = { + name: mockedRule.name, + description: mockedRule.description, + note: mockedRule.note, + }; - expect(scheduleRuleData?.from).toEqual('NaNh'); - }); + expect(result).toEqual(expected); + }); - test('returns from as 5m if interval is not parsable as dateMath', () => { - const mockedRule = { - ...mockRule('test-id'), - interval: 'randomstring', - }; - const { scheduleRuleData }: GetStepsData = getStepsData({ rule: mockedRule }); + 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( + mockedRule, + false + ); + const expected = { name: mockedRule.name, description: mockedRule.description, note: '' }; - expect(scheduleRuleData?.from).toEqual('5m'); - expect(scheduleRuleData?.interval).toEqual('randomstring'); - }); + expect(result).toEqual(expected); }); }); - describe('getStepsDataDetails', () => { - test('returns object with about, about details, define, and schedule step properties formatted', () => { - const { - defineRuleData, - aboutRuleData, - aboutRuleDataDetails, - scheduleRuleData, - }: GetStepsDataDetails = getStepsDataDetails(mockRuleWithEverything('test-id')); - const defineRuleStepData = { + describe('getDefineStepsData', () => { + test('returns with saved_id if value exists on rule', () => { + const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); + const expected = { isNew: false, index: ['auditbeat-*'], queryBar: { query: { - query: 'user.name: root or user.name: admin', + query: '', language: 'kuery', }, - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', + filters: [], + saved_id: "Garrett's IP", }, }; - const aboutRuleStepData = { - description: '', - falsePositives: ['test'], + + expect(result).toEqual(expected); + }); + + test('returns with saved_id of null if value does not exist on rule', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + delete mockedRule.saved_id; + const result: DefineStepRule = getDefineStepsData(mockedRule); + const expected = { isNew: false, - name: '', - note: '', - references: ['www.test.co'], - riskScore: 21, - severity: 'low', - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', }, - ], - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Titled timeline', + filters: [], + saved_id: null, }, }; + + expect(result).toEqual(expected); + }); + }); + + describe('getHumanizedDuration', () => { + test('returns from as seconds if from duration is less than a minute', () => { + const result = getHumanizedDuration('now-62s', '1m'); + + expect(result).toEqual('2s'); + }); + + test('returns from as minutes if from duration is less than an hour', () => { + const result = getHumanizedDuration('now-660s', '5m'); + + expect(result).toEqual('6m'); + }); + + test('returns from as hours if from duration is more than 60 minutes', () => { + const result = getHumanizedDuration('now-7400s', '5m'); + + expect(result).toEqual('1h'); + }); + + test('returns from as if from is not parsable as dateMath', () => { + const result = getHumanizedDuration('randomstring', '5m'); + + expect(result).toEqual('NaNh'); + }); + + test('returns from as 5m if interval is not parsable as dateMath', () => { + const result = getHumanizedDuration('now-300s', 'randomstring'); + + expect(result).toEqual('5m'); + }); + }); + + describe('getScheduleStepsData', () => { + test('returns expected ScheduleStep rule object', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + const result: ScheduleStepRule = getScheduleStepsData(mockedRule); + const expected = { + isNew: false, + enabled: mockedRule.enabled, + interval: mockedRule.interval, + from: '0s', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getModifiedAboutDetailsData', () => { + test('returns object with "note" and "description" being those of passed in rule', () => { + const result: AboutStepRuleDetails = getModifiedAboutDetailsData( + mockRuleWithEverything('test-id') + ); const aboutRuleDataDetailsData = { note: '# this is some markdown documentation', description: '24/7', }; - const scheduleRuleStepData = { enabled: true, from: '0s', interval: '5m', isNew: false }; - expect(defineRuleData).toEqual(defineRuleStepData); - expect(aboutRuleData).toEqual(aboutRuleStepData); - expect(aboutRuleDataDetails).toEqual(aboutRuleDataDetailsData); - expect(scheduleRuleData).toEqual(scheduleRuleStepData); + expect(result).toEqual(aboutRuleDataDetailsData); }); - test('returns aboutRuleDataDetails with empty string if "note" does not exist', () => { + test('returns "note" with empty string if "note" does not exist', () => { const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; - const { aboutRuleDataDetails }: GetStepsDataDetails = getStepsDataDetails( - mockRuleWithoutNote - ); + const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; - expect(aboutRuleDataDetails).toEqual(aboutRuleDetailsData); + expect(result).toEqual(aboutRuleDetailsData); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index a20a5139a173de..1fc8a86a476f2d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -5,7 +5,7 @@ */ import dateMath from '@elastic/datemath'; -import { get, pick } from 'lodash/fp'; +import { get } from 'lodash/fp'; import moment from 'moment'; import { useLocation } from 'react-router-dom'; @@ -21,15 +21,10 @@ import { } from './types'; export interface GetStepsData { - aboutRuleData: AboutStepRule | null; - defineRuleData: DefineStepRule | null; - scheduleRuleData: ScheduleStepRule | null; -} -export interface GetStepsDataDetails { - aboutRuleData: AboutStepRule | null; - aboutRuleDataDetails: AboutStepRuleDetails | null; - defineRuleData: DefineStepRule | null; - scheduleRuleData: ScheduleStepRule | null; + aboutRuleData: AboutStepRule; + modifiedAboutRuleDetailsData: AboutStepRuleDetails; + defineRuleData: DefineStepRule; + scheduleRuleData: ScheduleStepRule; } export const getStepsData = ({ @@ -39,85 +34,107 @@ export const getStepsData = ({ rule: Rule; detailsView?: boolean; }): GetStepsData => { - const defineRuleData: DefineStepRule | null = - rule != null - ? { - isNew: false, - index: rule.index, - queryBar: { - query: { query: rule.query as string, language: rule.language }, - filters: rule.filters as Filter[], - saved_id: rule.saved_id ?? null, - }, - } - : null; - const aboutRuleData: AboutStepRule | null = - rule != null - ? { - isNew: false, - ...pick(['description', 'name', 'references', 'severity', 'tags', 'threat'], rule), - ...(detailsView - ? { - name: '', - description: '', - note: '', - } - : { note: rule.note ?? '' }), - threat: rule.threat as IMitreEnterpriseAttack[], - falsePositives: rule.false_positives, - riskScore: rule.risk_score, - timeline: { - id: rule.timeline_id ?? null, - title: rule.timeline_title ?? null, - }, - } - : null; - - const from = dateMath.parse(rule.from) ?? moment(); - const interval = dateMath.parse(`now-${rule.interval}`) ?? moment(); - - const fromDuration = moment.duration(interval.diff(from)); - let fromHumanize = `${Math.floor(fromDuration.asHours())}h`; + const defineRuleData: DefineStepRule = getDefineStepsData(rule); + const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView); + const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule); + const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule); + + return { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData }; +}; + +export const getDefineStepsData = (rule: Rule): DefineStepRule => { + const { index, query, language, filters, saved_id: savedId } = rule; + + return { + isNew: false, + index, + queryBar: { + query: { + query, + language, + }, + filters: filters as Filter[], + saved_id: savedId ?? null, + }, + }; +}; + +export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { + const { enabled, interval, from } = rule; + const fromHumanizedValue = getHumanizedDuration(from, interval); + + return { + isNew: false, + enabled, + interval, + from: fromHumanizedValue, + }; +}; + +export const getHumanizedDuration = (from: string, interval: string): string => { + const fromValue = dateMath.parse(from) ?? moment(); + const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); + + const fromDuration = moment.duration(intervalValue.diff(fromValue)); + const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; if (fromDuration.asSeconds() < 60) { - fromHumanize = `${Math.floor(fromDuration.asSeconds())}s`; + return `${Math.floor(fromDuration.asSeconds())}s`; } else if (fromDuration.asMinutes() < 60) { - fromHumanize = `${Math.floor(fromDuration.asMinutes())}m`; + return `${Math.floor(fromDuration.asMinutes())}m`; } - const scheduleRuleData: ScheduleStepRule | null = - rule != null - ? { - isNew: false, - ...pick(['enabled', 'interval'], rule), - from: fromHumanize, - } - : null; - - return { aboutRuleData, defineRuleData, scheduleRuleData }; + return fromHumanize; }; -export const getStepsDataDetails = (rule: Rule): GetStepsDataDetails => { - const { defineRuleData, aboutRuleData, scheduleRuleData } = getStepsData({ - rule, - detailsView: true, - }); - const modifiedAboutStepRuleData: AboutStepRuleDetails | null = - rule != null - ? { - note: rule.note ?? '', - description: rule.description, - } - : null; +export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { + const { name, description, note } = determineDetailsValue(rule, detailsView); + const { + references, + severity, + false_positives: falsePositives, + risk_score: riskScore, + tags, + threat, + timeline_id: timelineId, + timeline_title: timelineTitle, + } = rule; return { - aboutRuleData, - aboutRuleDataDetails: modifiedAboutStepRuleData, - defineRuleData, - scheduleRuleData, + isNew: false, + name, + description, + note: note!, + references, + severity, + tags, + riskScore, + falsePositives, + threat: threat as IMitreEnterpriseAttack[], + timeline: { + id: timelineId ?? null, + title: timelineTitle ?? null, + }, }; }; +export const determineDetailsValue = ( + rule: Rule, + detailsView: boolean +): Pick => { + const { name, description, note } = rule; + if (detailsView) { + return { name: '', description: '', note: '' }; + } + + return { name, description, note: note ?? '' }; +}; + +export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ + note: rule.note ?? '', + description: rule.description, +}); + export const useQuery = () => new URLSearchParams(useLocation().search); export type PrePackagedRuleStatus =