diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index 21f5f271f..1b4124077 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -215,6 +215,19 @@ describe('Rules', () => { .contains(line, TWENTY_SECONDS_TIMEOUT) ); + cy.get( + '[data-test-subj="change-editor-type"] label:nth-child(2)', + TWENTY_SECONDS_TIMEOUT + ).click({ + force: true, + }); + + YAML_RULE_LINES.forEach((line) => + cy + .get('[data-test-subj="rule_flyout_yaml_rule"]', TWENTY_SECONDS_TIMEOUT) + .contains(line, TWENTY_SECONDS_TIMEOUT) + ); + // Close the flyout cy.get('[data-test-subj="euiFlyoutCloseButton"]', TWENTY_SECONDS_TIMEOUT).click({ force: true, diff --git a/public/pages/Rules/components/RuleContentViewer/RuleContentViewer.test.tsx b/public/pages/Rules/components/RuleContentViewer/RuleContentViewer.test.tsx new file mode 100644 index 000000000..aa5c20055 --- /dev/null +++ b/public/pages/Rules/components/RuleContentViewer/RuleContentViewer.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { RuleContentViewer } from './RuleContentViewer'; + +describe(' spec', () => { + it('renders the component', () => { + const { container } = render( + + ); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/public/pages/Rules/components/RuleContentViewer/RuleContentViewer.tsx b/public/pages/Rules/components/RuleContentViewer/RuleContentViewer.tsx index ca87404df..9c4e9e507 100644 --- a/public/pages/Rules/components/RuleContentViewer/RuleContentViewer.tsx +++ b/public/pages/Rules/components/RuleContentViewer/RuleContentViewer.tsx @@ -14,144 +14,183 @@ import { EuiModalBody, EuiSpacer, EuiText, + EuiButtonGroup, } from '@elastic/eui'; import { DEFAULT_EMPTY_DATA } from '../../../../utils/constants'; -import React from 'react'; +import React, { useState } from 'react'; import { RuleItemInfoBase } from '../../models/types'; +import { RuleContentYamlViewer } from './RuleContentYamlViewer'; export interface RuleContentViewerProps { rule: RuleItemInfoBase; } +const editorTypes = [ + { + id: 'visual', + label: 'Visual', + }, + { + id: 'yaml', + label: 'YAML', + }, +]; + export const RuleContentViewer: React.FC = ({ - rule: { prePackaged, _source: ruleData }, + rule: { prePackaged, _source: ruleData, _id: ruleId }, }) => { + if (!ruleData.id) { + ruleData.id = ruleId; + } + const [selectedEditorType, setSelectedEditorType] = useState('visual'); + + const onEditorTypeChange = (optionId: string) => { + setSelectedEditorType(optionId); + }; + return ( - - - Rule Name - {ruleData.title} - - - Log Type - {ruleData.category} - - - - - - Description - - {ruleData.description || DEFAULT_EMPTY_DATA} - - - - - - Last Updated - {ruleData.last_update_time} - - - Author - {ruleData.author} - - - - - - - - Source - {prePackaged ? 'Sigma' : 'Custom'} - - {prePackaged ? ( - - License - - Detection Rule License (DLR) - - - ) : null} - - - - - - - Rule level - {ruleData.level} - - - - - - Tags - {ruleData.tags.length > 0 ? ( - - {ruleData.tags.map((tag: any, i: number) => ( - - {tag.value} + onEditorTypeChange(id)} + /> + + {selectedEditorType === 'visual' && ( + <> + + + Rule Name + {ruleData.title} - ))} - - ) : ( -
{DEFAULT_EMPTY_DATA}
- )} + + Log Type + {ruleData.category} + +
+ + + + Description + + {ruleData.description || DEFAULT_EMPTY_DATA} + + + + + + Last Updated + {ruleData.last_update_time} + + + Author + {ruleData.author} + + - - - - References - {ruleData.references.length > 0 ? ( - ruleData.references.map((reference: any, i: number) => ( -
- - {reference.value} - - + + + + + Source + {prePackaged ? 'Sigma' : 'Custom'} + + {prePackaged ? ( + + License + + Detection Rule License (DLR) + + + ) : null} + + + + + + + Rule level + {ruleData.level} + + + + + + Tags + {ruleData.tags.length > 0 ? ( + + {ruleData.tags.map((tag: any, i: number) => ( + + {tag.value} + + ))} + + ) : ( +
{DEFAULT_EMPTY_DATA}
+ )} + + + + + References + {ruleData.references.length > 0 ? ( + ruleData.references.map((reference: any, i: number) => ( +
+ + {reference.value} + + +
+ )) + ) : ( +
{DEFAULT_EMPTY_DATA}
+ )} + + + + False positive cases +
+ {ruleData.false_positives.length > 0 ? ( + ruleData.false_positives.map((falsepositive: any, i: number) => ( +
+ {falsepositive.value} + +
+ )) + ) : ( +
{DEFAULT_EMPTY_DATA}
+ )}
- )) - ) : ( -
{DEFAULT_EMPTY_DATA}
- )} - - - False positive cases -
- {ruleData.false_positives.length > 0 ? ( - ruleData.false_positives.map((falsepositive: any, i: number) => ( -
- {falsepositive.value} - -
- )) - ) : ( -
{DEFAULT_EMPTY_DATA}
- )} -
- - - - Rule Status -
{ruleData.status}
- - - - - - {ruleData.detection} - - + + + Rule Status +
{ruleData.status}
+ + + + + + {ruleData.detection} + + + + )} + {selectedEditorType === 'yaml' && ( + + + + )} ); }; diff --git a/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.test.tsx b/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.test.tsx new file mode 100644 index 000000000..8c1571c8d --- /dev/null +++ b/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { RuleContentYamlViewer } from './RuleContentYamlViewer'; + +describe(' spec', () => { + it('renders the component', () => { + const { container } = render( + + ); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.tsx b/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.tsx new file mode 100644 index 000000000..bf92ea056 --- /dev/null +++ b/public/pages/Rules/components/RuleContentViewer/RuleContentYamlViewer.tsx @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiCodeBlock } from '@elastic/eui'; +import React from 'react'; +import { mapRuleToYamlObject, mapYamlObjectToYamlString } from '../../utils/mappers'; +import { Rule } from '../../../../../models/interfaces'; + +export interface RuleContentYamlViewerProps { + rule: Rule; +} + +export const RuleContentYamlViewer: React.FC = ({ rule }) => { + const yamlObject = mapRuleToYamlObject(rule); + const ruleYaml = mapYamlObjectToYamlString(yamlObject); + + return ( + + {ruleYaml} + + ); +}; diff --git a/public/pages/Rules/components/RuleContentViewer/__snapshots__/RuleContentViewer.test.tsx.snap b/public/pages/Rules/components/RuleContentViewer/__snapshots__/RuleContentViewer.test.tsx.snap new file mode 100644 index 000000000..891a04d01 --- /dev/null +++ b/public/pages/Rules/components/RuleContentViewer/__snapshots__/RuleContentViewer.test.tsx.snap @@ -0,0 +1,341 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +
+
+
+ + This is editor type selector + +
+ + +
+
+
+
+
+ +
+ My Rule +
+
+
+ +
+ dns +
+
+
+
+ +
+ My Rule +
+
+
+
+ + 2022-11-22T23:00:00.000Z +
+
+ + aleksandar +
+
+
+
+
+ + Custom +
+
+
+
+
+ + high +
+
+
+ +
+ - +
+
+
+ +
+ - +
+
+ +
+
+ - +
+
+
+ +
+ stable +
+
+
+
+ +
+
+
+
+            
+              
+                
+                  selection
+                
+                
+                  :
+                
+                
+
+              
+              
+                  
+                
+                  EventID
+                
+                
+                  :
+                
+                 
+                
+                  4800
+                
+                
+
+              
+              
+                
+                
+                  condition
+                
+                
+                  :
+                
+                 selection
+
+              
+              
+                
+              
+            
+          
+
+
+
+
+
+`; diff --git a/public/pages/Rules/components/RuleContentViewer/__snapshots__/RuleContentYamlViewer.test.tsx.snap b/public/pages/Rules/components/RuleContentViewer/__snapshots__/RuleContentYamlViewer.test.tsx.snap new file mode 100644 index 000000000..4dd890090 --- /dev/null +++ b/public/pages/Rules/components/RuleContentViewer/__snapshots__/RuleContentYamlViewer.test.tsx.snap @@ -0,0 +1,407 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +
+
+    
+      
+        
+          id
+        
+        
+          :
+        
+         25b9c01c
+        
+          -
+        
+        350d
+        
+          -
+        
+        4b95
+        
+          -
+        
+        bed1
+        
+          -
+        
+        836d04a4f324
+
+      
+      
+        
+        
+          logsource
+        
+        
+          :
+        
+        
+
+      
+      
+          
+        
+          product
+        
+        
+          :
+        
+         windows
+
+      
+      
+        
+        
+          title
+        
+        
+          :
+        
+         Testing rule
+
+      
+      
+        
+        
+          description
+        
+        
+          :
+        
+         Testing Description
+
+      
+      
+        
+        
+          tags
+        
+        
+          :
+        
+        
+
+      
+      
+          
+        
+          -
+        
+         attack.persistence
+
+      
+      
+          
+        
+          -
+        
+         attack.privilege_escalation
+
+      
+      
+          
+        
+          -
+        
+         attack.t1543.003
+
+      
+      
+        
+        
+          falsepositives
+        
+        
+          :
+        
+        
+
+      
+      
+          
+        
+          -
+        
+         Unknown
+
+      
+      
+        
+        
+          level
+        
+        
+          :
+        
+         high
+
+      
+      
+        
+        
+          status
+        
+        
+          :
+        
+         experimental
+
+      
+      
+        
+        
+          references
+        
+        
+          :
+        
+        
+
+      
+      
+          
+        
+          -
+        
+         
+        
+          'https://securelist.com/operation-tunnelsnake-and-moriya-rootkit/101831'
+        
+        
+
+      
+      
+        
+        
+          author
+        
+        
+          :
+        
+         Bhabesh Raj
+
+      
+      
+        
+        
+          detection
+        
+        
+          :
+        
+        
+
+      
+      
+          
+        
+          selection
+        
+        
+          :
+        
+        
+
+      
+      
+            
+        
+          Provider_Name
+        
+        
+          :
+        
+         Service Control Manager
+
+      
+      
+            
+        
+          EventID
+        
+        
+          :
+        
+         
+        
+          7045
+        
+        
+
+      
+      
+            
+        
+          ServiceName
+        
+        
+          :
+        
+         ZzNetSvc
+
+      
+      
+          
+        
+          condition
+        
+        
+          :
+        
+         selection
+
+      
+      
+        
+      
+    
+  
+
+`; diff --git a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx index abdb234a0..842faffef 100644 --- a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx @@ -4,7 +4,7 @@ */ import React, { useState } from 'react'; -import { dump, load } from 'js-yaml'; +import { load } from 'js-yaml'; import { EuiFormRow, EuiCodeEditor, EuiLink, EuiSpacer, EuiText, EuiForm } from '@elastic/eui'; import FormFieldHeader from '../../../../components/FormFieldHeader'; import { Rule } from '../../../../../models/interfaces'; @@ -16,6 +16,11 @@ import { descriptionErrorString, titleErrorString, } from '../../../../utils/validation'; +import { + mapRuleToYamlObject, + mapYamlObjectToYamlString, + mapYamlObjectToRule, +} from '../../utils/mappers'; export interface YamlRuleEditorProps { rule: Rule; @@ -27,74 +32,6 @@ export interface YamlEditorState { value?: string; } -const mapYamlObjectToYamlString = (rule: Rule): string => { - try { - if (!rule.detection) { - const { detection, ...ruleWithoutDetection } = rule; - return dump(ruleWithoutDetection); - } else { - return dump(rule); - } - } catch (error: any) { - console.warn('Security Analytics - Rule Eritor - Yaml dump', error); - return ''; - } -}; - -const mapRuleToYamlObject = (rule: Rule): any => { - let detection = undefined; - if (rule.detection) { - try { - detection = load(rule.detection); - } catch {} - } - - const yamlObject: any = { - id: rule.id, - logsource: { product: rule.category }, - title: rule.title, - description: rule.description, - tags: rule.tags.map((tag) => tag.value), - falsepositives: rule.false_positives.map((falsePositive) => falsePositive.value), - level: rule.level, - status: rule.status, - references: rule.references.map((reference) => reference.value), - author: rule.author, - detection, - }; - - return yamlObject; -}; - -const mapYamlObjectToRule = (obj: any): Rule => { - let detection = ''; - if (obj.detection) { - try { - detection = dump(obj.detection); - } catch {} - } - const rule: Rule = { - id: obj.id, - category: obj.logsource ? obj.logsource.product : undefined, - log_source: '', - title: obj.title, - description: obj.description, - tags: obj.tags ? obj.tags.map((tag: string) => ({ value: tag })) : undefined, - false_positives: obj.falsepositives - ? obj.falsepositives.map((falsePositive: string) => ({ value: falsePositive })) - : undefined, - level: obj.level, - status: obj.status, - references: obj.references - ? obj.references.map((reference: string) => ({ value: reference })) - : undefined, - author: obj.author, - detection, - }; - - return rule; -}; - const validateRule = (rule: Rule): string[] | null => { const requiredFiledsValidationErrors: Array = []; diff --git a/public/pages/Rules/utils/mappers.ts b/public/pages/Rules/utils/mappers.ts new file mode 100644 index 000000000..4956865aa --- /dev/null +++ b/public/pages/Rules/utils/mappers.ts @@ -0,0 +1,70 @@ +import { dump, load } from 'js-yaml'; +import { Rule } from '../../../../models/interfaces'; + +export const mapYamlObjectToYamlString = (rule: Rule): string => { + try { + if (!rule.detection) { + const { detection, ...ruleWithoutDetection } = rule; + return dump(ruleWithoutDetection); + } else { + return dump(rule); + } + } catch (error: any) { + console.warn('Security Analytics - Rule Eritor - Yaml dump', error); + return ''; + } +}; + +export const mapRuleToYamlObject = (rule: Rule): any => { + let detection = undefined; + if (rule.detection) { + try { + detection = load(rule.detection); + } catch {} + } + + const yamlObject: any = { + id: rule.id, + logsource: { product: rule.category }, + title: rule.title, + description: rule.description, + tags: rule.tags.map((tag) => tag.value), + falsepositives: rule.false_positives.map((falsePositive) => falsePositive.value), + level: rule.level, + status: rule.status, + references: rule.references.map((reference) => reference.value), + author: rule.author, + detection, + }; + + return yamlObject; +}; + +export const mapYamlObjectToRule = (obj: any): Rule => { + let detection = ''; + if (obj.detection) { + try { + detection = dump(obj.detection); + } catch {} + } + const rule: Rule = { + id: obj.id, + category: obj.logsource ? obj.logsource.product : undefined, + log_source: '', + title: obj.title, + description: obj.description, + tags: obj.tags ? obj.tags.map((tag: string) => ({ value: tag })) : undefined, + false_positives: obj.falsepositives + ? obj.falsepositives.map((falsePositive: string) => ({ value: falsePositive })) + : undefined, + level: obj.level, + status: obj.status, + references: obj.references + ? obj.references.map((reference: string) => ({ value: reference })) + : undefined, + author: obj.author, + detection, + }; + + return rule; +};