From af8e710aa1b721635a3ebe00cd91ab5ff4519121 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Thu, 5 Oct 2023 11:26:58 -0700 Subject: [PATCH] Added categories for log types (#741) * added categories for log types Signed-off-by: Amardeepsingh Siglani * fixed cypress tests Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani (cherry picked from commit 3ce112b70722b68a9897950dd4d25f8d66060ae6) --- cypress/integration/2_rules.spec.js | 12 +-- cypress/integration/3_alerts.spec.js | 2 +- cypress/integration/4_findings.spec.js | 2 +- cypress/support/helpers.js | 5 +- .../components/Utility/LogCategoryOption.tsx | 21 ++++++ .../containers/CreateCorrelationRule.tsx | 14 ++-- public/pages/Correlations/utils/helpers.tsx | 8 +- .../components/DetectorType/DetectorType.tsx | 22 ++++-- .../containers/Detectors/Detectors.tsx | 8 +- .../__snapshots__/Detectors.test.tsx.snap | 38 +++------- .../pages/LogTypes/components/LogTypeForm.tsx | 36 +++++++-- public/pages/LogTypes/utils/constants.ts | 1 + public/pages/LogTypes/utils/helpers.tsx | 17 +++++ .../components/RuleEditor/RuleEditorForm.tsx | 12 ++- public/pages/Rules/utils/constants.ts | 2 +- public/pages/Rules/utils/helpers.tsx | 19 +++-- public/store/LogTypeStore.ts | 28 +++++-- public/utils/{constants.ts => constants.tsx} | 29 +++++++- public/utils/helpers.tsx | 74 ++++++++++++++++++- types/LogTypes.ts | 1 + 20 files changed, 261 insertions(+), 90 deletions(-) create mode 100644 public/components/Utility/LogCategoryOption.tsx rename public/utils/{constants.ts => constants.tsx} (82%) diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index fde8195c6..89544f177 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -11,7 +11,7 @@ const SAMPLE_RULE = { logType: 'windows', description: 'This is a rule used to test the rule creation workflow.', detectionLine: ['condition: Selection_1', 'Selection_1:', 'FieldKey|contains:', '- FieldValue'], - severity: 'critical', + severity: 'Critical', tags: ['attack.persistence', 'attack.privilege_escalation', 'attack.t1543.003'], references: 'https://nohello.com', falsePositive: 'unknown', @@ -31,7 +31,7 @@ const YAML_RULE_LINES = [ `- ${SAMPLE_RULE.tags[2]}`, `falsepositives:`, `- ${SAMPLE_RULE.falsePositive}`, - `level: ${SAMPLE_RULE.severity}`, + `level: ${SAMPLE_RULE.severity.toLowerCase()}`, `status: ${SAMPLE_RULE.status}`, `references:`, `- '${SAMPLE_RULE.references}'`, @@ -67,7 +67,9 @@ const checkRulesFlyout = () => { cy.get('[data-test-subj="rule_flyout_rule_source"]').contains('Custom'); // Validate severity - cy.get('[data-test-subj="rule_flyout_rule_severity"]').contains(SAMPLE_RULE.severity); + cy.get('[data-test-subj="rule_flyout_rule_severity"]').contains( + SAMPLE_RULE.severity.toLowerCase() + ); // Validate tags SAMPLE_RULE.tags.forEach((tag) => @@ -159,7 +161,7 @@ const fillCreateForm = () => { getAuthorField().type(`${SAMPLE_RULE.author}`); // rule details - getLogTypeField().type(SAMPLE_RULE.logType); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); getRuleLevelField().selectComboboxItem(SAMPLE_RULE.severity); // rule detection @@ -548,7 +550,7 @@ describe('Rules', () => { SAMPLE_RULE.logType = 'dns'; YAML_RULE_LINES[2] = `product: ${SAMPLE_RULE.logType}`; YAML_RULE_LINES[3] = `title: ${SAMPLE_RULE.name}`; - getLogTypeField().type(SAMPLE_RULE.logType).type('{enter}'); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); getLogTypeField().containsValue(SAMPLE_RULE.logType).contains(SAMPLE_RULE.logType); SAMPLE_RULE.description += ' edited'; diff --git a/cypress/integration/3_alerts.spec.js b/cypress/integration/3_alerts.spec.js index 3476d78e0..81341897f 100644 --- a/cypress/integration/3_alerts.spec.js +++ b/cypress/integration/3_alerts.spec.js @@ -118,7 +118,7 @@ describe('Alerts', () => { expect($tr, `timestamp`).to.contain(date); expect($tr, `rule name`).to.contain('Cypress USB Rule'); expect($tr, `detector name`).to.contain(testDetector.name); - expect($tr, `log type`).to.contain('windows'); + expect($tr, `log type`).to.contain('System Activity: Windows'); }); // Close the flyout diff --git a/cypress/integration/4_findings.spec.js b/cypress/integration/4_findings.spec.js index 59114b36a..a2a2787bb 100644 --- a/cypress/integration/4_findings.spec.js +++ b/cypress/integration/4_findings.spec.js @@ -52,7 +52,7 @@ describe('Findings', () => { cy.contains('No items found').should('not.exist'); // Check for expected findings - cy.contains('windows'); + cy.contains('System Activity: Windows'); cy.contains('High'); }); diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js index c3b990892..33779103e 100644 --- a/cypress/support/helpers.js +++ b/cypress/support/helpers.js @@ -62,7 +62,10 @@ Cypress.Commands.add( items = [items]; } Cypress.log({ message: `Select combobox items: ${items.join(' | ')}` }); - items.map((item) => cy.wrap(subject).type(item).type('{enter}')); + items.map((item) => { + cy.wrap(subject).type(item); + cy.get(`[title="${item}"]`).click({ force: true }); + }); } ); diff --git a/public/components/Utility/LogCategoryOption.tsx b/public/components/Utility/LogCategoryOption.tsx new file mode 100644 index 000000000..afe18298f --- /dev/null +++ b/public/components/Utility/LogCategoryOption.tsx @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiTitle } from '@elastic/eui'; +import React, { useEffect, useRef } from 'react'; + +export const LogCategoryOptionView: React.FC<{ categoryName: string }> = ({ categoryName }) => { + const inputRef = useRef(null); + + useEffect(() => { + inputRef.current?.closest('button.euiFilterSelectItem')?.setAttribute('disabled', 'true'); + }, [inputRef.current]); + + return ( + +

{categoryName}

+
+ ); +}; diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index 983ed7fc3..c986eb4d1 100644 --- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx +++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx @@ -38,7 +38,7 @@ import { CoreServicesContext } from '../../../components/core_services'; import { RouteComponentProps, useParams } from 'react-router-dom'; import { validateName } from '../../../utils/validation'; import { FieldMappingService, IndexService } from '../../../services'; -import { errorNotificationToast } from '../../../utils/helpers'; +import { errorNotificationToast, getLogTypeOptions } from '../../../utils/helpers'; export interface CreateCorrelationRuleProps { indexService: IndexService; @@ -103,6 +103,7 @@ export const CreateCorrelationRule: React.FC = ( ...correlationRuleStateDefaultValue, }); const [action, setAction] = useState('Create'); + const [logTypeOptions, setLogTypeOptions] = useState([]); useEffect(() => { if (props.history.location.state?.rule) { @@ -119,6 +120,12 @@ export const CreateCorrelationRule: React.FC = ( setAction('Edit'); setInitialRuleValues(); } + + const setupLogTypeOptions = async () => { + const options = await getLogTypeOptions(); + setLogTypeOptions(options); + }; + setupLogTypeOptions(); }, []); const submit = async (values: any) => { @@ -274,10 +281,7 @@ export const CreateCorrelationRule: React.FC = ( isInvalid={isInvalidInputForQuery('logType')} placeholder="Select a log type" data-test-subj={'rule_type_dropdown'} - options={ruleTypes.map(({ label }) => ({ - value: label.toLowerCase(), - label, - }))} + options={logTypeOptions} singleSelection={{ asPlainText: true }} onChange={(e) => { props.handleChange(`queries[${queryIdx}].logType`)( diff --git a/public/pages/Correlations/utils/helpers.tsx b/public/pages/Correlations/utils/helpers.tsx index b06d07ecf..4c1633f23 100644 --- a/public/pages/Correlations/utils/helpers.tsx +++ b/public/pages/Correlations/utils/helpers.tsx @@ -12,9 +12,8 @@ import { CorrelationRuleQuery, } from '../../../../types'; import { Search } from '@opensearch-project/oui/src/eui_components/basic_table'; -import { ruleTypes } from '../../Rules/utils/constants'; import { FieldClause } from '@opensearch-project/oui/src/eui_components/search_bar/query/ast'; -import { formatRuleType } from '../../../utils/helpers'; +import { formatRuleType, getLogTypeFilterOptions } from '../../../utils/helpers'; export const getCorrelationRulesTableColumns = ( onRuleNameClick: (rule: CorrelationRule) => void, @@ -110,10 +109,7 @@ export const getCorrelationRulesTableSearchConfig = ( field: 'logTypes', name: 'Log Types', multiSelect: 'or', - options: ruleTypes.map(({ value, label }) => ({ - value, - name: label, - })), + options: getLogTypeFilterOptions(), }, ], }; diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorType/DetectorType.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorType/DetectorType.tsx index 706d99af1..0b98da1f9 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorType/DetectorType.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorType/DetectorType.tsx @@ -12,6 +12,7 @@ import { RuleItem } from '../DetectionRules/types/interfaces'; import { ruleTypes } from '../../../../../Rules/utils/constants'; import ConfigureFieldMapping from '../../../ConfigureFieldMapping'; import { ConfigureFieldMappingProps } from '../../../ConfigureFieldMapping/containers/ConfigureFieldMapping'; +import { getLogTypeOptions } from '../../../../../../utils/helpers'; interface DetectorTypeProps { detectorType: string; @@ -30,18 +31,23 @@ interface DetectorTypeState { } export default class DetectorType extends Component { - private detectorTypeOptions: { value: string; label: string }[]; + private detectorTypeOptions: { value: string; label: string }[] = []; constructor(props: DetectorTypeProps) { super(props); - this.detectorTypeOptions = ruleTypes.map(({ label }) => ({ value: label, label })); - const detectorTypeIds = this.detectorTypeOptions.map((option) => option.value); this.state = { fieldTouched: false, - detectorTypeIds, + detectorTypeIds: [], }; } + async componentDidMount(): Promise { + this.detectorTypeOptions = await getLogTypeOptions(); + this.setState({ + detectorTypeIds: ruleTypes.map((option) => option.value), + }); + } + onChange = (detectorType: string) => { this.setState({ fieldTouched: true }); this.props.onDetectorTypeChange(detectorType); @@ -66,7 +72,13 @@ export default class DetectorType extends Component + diff --git a/public/pages/Detectors/containers/Detectors/Detectors.tsx b/public/pages/Detectors/containers/Detectors/Detectors.tsx index 56890d3ab..df7fb0296 100644 --- a/public/pages/Detectors/containers/Detectors/Detectors.tsx +++ b/public/pages/Detectors/containers/Detectors/Detectors.tsx @@ -28,6 +28,7 @@ import { capitalizeFirstLetter, errorNotificationToast, formatRuleType, + getLogTypeFilterOptions, renderTime, } from '../../../../utils/helpers'; import { CoreServicesContext } from '../../../../components/core_services'; @@ -298,11 +299,11 @@ export default class Detectors extends Component detectorHits.map((detector) => (detector._source.enabled ? 'Active' : 'Inactive')) ), ]; - const logType = [...new Set(detectorHits.map((detector) => detector._source.detector_type))]; const search = { box: { placeholder: 'Search threat detectors', schema: true, + incremental: true, }, filters: [ { @@ -319,10 +320,7 @@ export default class Detectors extends Component type: 'field_value_selection', field: 'logType', name: 'Log type', - options: logType.map((logType) => ({ - value: logType, - name: formatRuleType(logType), - })), + options: getLogTypeFilterOptions(), multiSelect: 'or', } as FieldValueSelectionFilterConfigType, ], diff --git a/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap b/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap index 1c0af37da..6419970e4 100644 --- a/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap +++ b/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap @@ -578,6 +578,7 @@ exports[` spec renders the component 1`] = ` search={ Object { "box": Object { + "incremental": true, "placeholder": "Search threat detectors", "schema": true, }, @@ -598,12 +599,7 @@ exports[` spec renders the component 1`] = ` "field": "logType", "multiSelect": "or", "name": "Log type", - "options": Array [ - Object { - "name": "-", - "value": "detector_type", - }, - ], + "options": Array [], "type": "field_value_selection", }, ], @@ -628,6 +624,7 @@ exports[` spec renders the component 1`] = ` spec renders the component 1`] = ` "field": "logType", "multiSelect": "or", "name": "Log type", - "options": Array [ - Object { - "name": "-", - "value": "detector_type", - }, - ], + "options": Array [], "type": "field_value_selection", }, ] @@ -697,18 +689,18 @@ exports[` spec renders the component 1`] = ` className="euiFlexItem euiSearchBar__searchHolder" > spec renders the component 1`] = ` isInvalid={false} > spec renders the component 1`] = ` "field": "logType", "multiSelect": "or", "name": "Log type", - "options": Array [ - Object { - "name": "-", - "value": "detector_type", - }, - ], + "options": Array [], "type": "field_value_selection", }, ] @@ -975,12 +962,7 @@ exports[` spec renders the component 1`] = ` "field": "logType", "multiSelect": "or", "name": "Log type", - "options": Array [ - Object { - "name": "-", - "value": "detector_type", - }, - ], + "options": Array [], "type": "field_value_selection", } } diff --git a/public/pages/LogTypes/components/LogTypeForm.tsx b/public/pages/LogTypes/components/LogTypeForm.tsx index 36e07b48c..c2e302082 100644 --- a/public/pages/LogTypes/components/LogTypeForm.tsx +++ b/public/pages/LogTypes/components/LogTypeForm.tsx @@ -12,6 +12,7 @@ import { EuiFlexItem, EuiFormRow, EuiSpacer, + EuiSuperSelect, EuiTextArea, } from '@elastic/eui'; import { LogTypeItem } from '../../../../types'; @@ -19,6 +20,7 @@ import React from 'react'; import { LOG_TYPE_NAME_REGEX, validateName } from '../../../utils/validation'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { useState } from 'react'; +import { logTypeCategoryOptions } from '../../../utils/constants'; export interface LogTypeFormProps { logTypeDetails: LogTypeItem; @@ -40,17 +42,21 @@ export const LogTypeForm: React.FC = ({ onConfirm, }) => { const [nameError, setNameError] = useState(''); + const [categoryError, setCategoryError] = useState(''); + const [categoryTouched, setCategoryTouched] = useState(false); - const updateErrors = (details = logTypeDetails) => { + const updateErrors = (details: LogTypeItem, onSubmit = false) => { const nameInvalid = !validateName(details.name, LOG_TYPE_NAME_REGEX, false /* shouldTrim */); + const categoryInvalid = (categoryTouched || onSubmit) && !details.category; setNameError(nameInvalid ? 'Invalid name' : ''); + setCategoryError(categoryInvalid ? 'Select category to assign' : ''); - return { nameInvalid }; + return { nameInvalid, categoryInvalid }; }; const onConfirmClicked = () => { - const { nameInvalid } = updateErrors(); + const { nameInvalid, categoryInvalid } = updateErrors(logTypeDetails, true); - if (nameInvalid) { + if (nameInvalid || categoryInvalid) { notifications?.toasts.addDanger({ title: `Failed to ${confirmButtonText.toLowerCase()}`, text: `Fix the marked errors.`, @@ -83,7 +89,6 @@ export const LogTypeForm: React.FC = ({ setLogTypeDetails(newLogType); updateErrors(newLogType); }} - placeholder="Enter name for the log type" readOnly={!isEditMode} disabled={isEditMode && !!logTypeDetails.detectionRulesCount} /> @@ -111,6 +116,27 @@ export const LogTypeForm: React.FC = ({ readOnly={!isEditMode} /> + + + ({ + ...option, + disabled: !isEditMode || (isEditMode && !!logTypeDetails.detectionRulesCount), + }))} + valueOfSelected={logTypeDetails?.category} + onChange={(value) => { + const newLogType = { + ...logTypeDetails, + category: value, + }; + setCategoryTouched(true); + setLogTypeDetails(newLogType); + updateErrors(newLogType); + }} + hasDividers + itemLayoutAlign="top" + > + {isEditMode ? ( diff --git a/public/pages/LogTypes/utils/constants.ts b/public/pages/LogTypes/utils/constants.ts index 865e35e64..5f672935d 100644 --- a/public/pages/LogTypes/utils/constants.ts +++ b/public/pages/LogTypes/utils/constants.ts @@ -21,4 +21,5 @@ export const defaultLogType: LogTypeBase = { description: '', source: 'Custom', tags: null, + category: '', }; diff --git a/public/pages/LogTypes/utils/helpers.tsx b/public/pages/LogTypes/utils/helpers.tsx index 0ade054fc..3c09c9803 100644 --- a/public/pages/LogTypes/utils/helpers.tsx +++ b/public/pages/LogTypes/utils/helpers.tsx @@ -9,6 +9,7 @@ import { LogType } from '../../../../types'; import { capitalize } from 'lodash'; import { Search } from '@opensearch-project/oui/src/eui_components/basic_table'; import { ruleSource } from '../../Rules/utils/constants'; +import { logTypesByCategories } from '../../../utils/constants'; export const getLogTypesTableColumns = ( showDetails: (id: string) => void, @@ -27,6 +28,11 @@ export const getLogTypesTableColumns = ( name: 'Description', truncateText: false, }, + { + field: 'category', + name: 'Category', + truncateText: false, + }, { field: 'source', name: 'Source', @@ -60,6 +66,17 @@ export const getLogTypesTableSearchConfig = (): Search => { schema: true, }, filters: [ + { + type: 'field_value_selection', + field: 'category', + name: 'Category', + multiSelect: 'or', + options: Object.keys(logTypesByCategories) + .map((category) => ({ + value: category, + })) + .sort((a, b) => (a.value < b.value ? -1 : a.value > b.value ? 1 : 0)), + }, { type: 'field_value_selection', field: 'source', diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx index ba652c321..5f883e3d2 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx @@ -23,7 +23,7 @@ import { } from '@elastic/eui'; import { ContentPanel } from '../../../../components/ContentPanel'; import { FieldTextArray } from './components/FieldTextArray'; -import { ruleStatus, ruleTypes } from '../../utils/constants'; +import { ruleStatus } from '../../utils/constants'; import { AUTHOR_REGEX, validateDescription, validateName } from '../../../../utils/validation'; import { RuleEditorFormModel } from './RuleEditorFormModel'; import { FormSubmissionErrorToastNotification } from './FormSubmitionErrorToastNotification'; @@ -31,7 +31,7 @@ import { YamlRuleEditorComponent } from './components/YamlRuleEditorComponent/Ya import { mapFormToRule, mapRuleToForm } from './mappers'; import { DetectionVisualEditor } from './DetectionVisualEditor'; import { useCallback } from 'react'; -import { DataStore } from '../../../../store/DataStore'; +import { getLogTypeOptions } from '../../../../utils/helpers'; export interface VisualRuleEditorProps { initialValue: RuleEditorFormModel; @@ -65,17 +65,15 @@ export const RuleEditorForm: React.FC = ({ }) => { const [selectedEditorType, setSelectedEditorType] = useState('visual'); const [isDetectionInvalid, setIsDetectionInvalid] = useState(false); - const [logTypeOptions, setLogTypeOptions] = useState( - ruleTypes.map(({ value, label }) => ({ value, label })) - ); + const [logTypeOptions, setLogTypeOptions] = useState([]); const onEditorTypeChange = (optionId: string) => { setSelectedEditorType(optionId); }; const refreshLogTypeOptions = useCallback(async () => { - const logTypes = await DataStore.logTypes.getLogTypes(); - setLogTypeOptions(logTypes.map(({ id, name }) => ({ value: id, label: name }))); + const logTypeOptions = await getLogTypeOptions(); + setLogTypeOptions(logTypeOptions); }, []); const validateTags = (fields: string[]) => { diff --git a/public/pages/Rules/utils/constants.ts b/public/pages/Rules/utils/constants.ts index 39b1c402c..377763448 100644 --- a/public/pages/Rules/utils/constants.ts +++ b/public/pages/Rules/utils/constants.ts @@ -5,7 +5,7 @@ import { euiPaletteForStatus } from '@elastic/eui'; -export const ruleTypes: { label: string; value: string; id: string }[] = []; +export const ruleTypes: { label: string; value: string; id: string; category: string }[] = []; const paletteColors = euiPaletteForStatus(5); diff --git a/public/pages/Rules/utils/helpers.tsx b/public/pages/Rules/utils/helpers.tsx index dee56a958..128dd7536 100644 --- a/public/pages/Rules/utils/helpers.tsx +++ b/public/pages/Rules/utils/helpers.tsx @@ -5,14 +5,18 @@ import { EuiBasicTableColumn, EuiBreadcrumb, EuiLink, EuiBadge } from '@elastic/eui'; import React from 'react'; -import { errorNotificationToast } from '../../../utils/helpers'; -import { ruleSeverity, ruleSource, ruleTypes } from './constants'; +import { + errorNotificationToast, + formatRuleType, + getLogTypeFilterOptions, +} from '../../../utils/helpers'; +import { ruleSeverity, ruleSource } from './constants'; import { Search } from '@opensearch-project/oui/src/eui_components/basic_table'; import { Rule } from '../../../../models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { AUTHOR_REGEX, validateDescription, validateName } from '../../../utils/validation'; import { dump, load } from 'js-yaml'; -import { BREADCRUMBS, DEFAULT_EMPTY_DATA } from '../../../utils/constants'; +import { BREADCRUMBS } from '../../../utils/constants'; import { RuleItemInfoBase, RulesTableColumnFields } from '../../../../types'; import { getSeverityColor, getSeverityLabel } from '../../Correlations/utils/constants'; @@ -65,9 +69,7 @@ export const getRulesTableColumns = ( sortable: true, width: '10%', truncateText: true, - render: (category: string) => - ruleTypes.find((ruleType) => ruleType.label.toLowerCase() === category)?.label || - DEFAULT_EMPTY_DATA, + render: (category: string) => formatRuleType(category), }, source: { field: 'source', @@ -107,10 +109,7 @@ export const getRulesTableSearchConfig = (): Search => { field: 'category', name: 'Log type', multiSelect: 'or', - options: ruleTypes.map(({ value, label }) => ({ - value, - name: label, - })), + options: getLogTypeFilterOptions(), }, { type: 'field_value_selection', diff --git a/public/store/LogTypeStore.ts b/public/store/LogTypeStore.ts index 42be73015..2e49a5725 100644 --- a/public/store/LogTypeStore.ts +++ b/public/store/LogTypeStore.ts @@ -9,6 +9,7 @@ import LogTypeService from '../services/LogTypeService'; import { errorNotificationToast } from '../utils/helpers'; import { DataStore } from './DataStore'; import { ruleTypes } from '../pages/Rules/utils/constants'; +import { logTypesByCategories } from '../utils/constants'; export class LogTypeStore { constructor(private service: LogTypeService, private notifications: NotificationsStart) {} @@ -53,16 +54,25 @@ export class LogTypeStore { 0, ruleTypes.length, ...logTypes - .map((logType) => ({ - label: logType.name, - value: logType.name, - id: logType.id, + .map(({ category, id, name }) => ({ + label: name, + value: name, + id, + category, })) .sort((a, b) => { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; }) ); + // Set log category types + for (const key in logTypesByCategories) { + delete logTypesByCategories[key]; + } + logTypes.forEach((logType) => { + logTypesByCategories[logType.category] = logTypesByCategories[logType.category] || []; + logTypesByCategories[logType.category].push(logType); + }); return logTypes; } @@ -79,12 +89,20 @@ export class LogTypeStore { return createRes.ok; } - public async updateLogType({ id, name, description, source, tags }: LogType): Promise { + public async updateLogType({ + category, + id, + name, + description, + source, + tags, + }: LogType): Promise { const updateRes = await this.service.updateLogType(id, { name, description, source, tags, + category, }); if (!updateRes.ok) { diff --git a/public/utils/constants.ts b/public/utils/constants.tsx similarity index 82% rename from public/utils/constants.ts rename to public/utils/constants.tsx index 9e3568a80..0acfc360c 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.tsx @@ -3,10 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React from 'react'; import { SimpleSavedObject } from 'opensearch-dashboards/public'; -import { Detector, ServerResponse } from '../../types'; +import { Detector, LogType, ServerResponse } from '../../types'; import { DetectorInput, PeriodSchedule } from '../../models/interfaces'; import { DetectorHit } from '../../server/models/interfaces'; +import { EuiText } from '@elastic/eui'; +import _ from 'lodash'; export const DATE_MATH_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; export const MAX_RECENTLY_USED_TIME_RANGES = 5; @@ -173,3 +176,27 @@ export const logTypesWithDashboards = new Set(['network', 'cloudtrail', 's3']); export const pendingDashboardCreations: { [detectorId: string]: undefined | Promise>>; } = {}; + +const logTypeCategoryInfo = [ + { name: 'Access Management', description: 'User access, authentication, group management' }, + { name: 'Applications', description: 'Application lifecycle, API, and web resources activities' }, + { name: 'Cloud Services', description: 'Services managed by cloud providers' }, + { name: 'Network Activity', description: 'DNS, HTTP, Email, SSH, FTP, DHCP, RDP' }, + { name: 'System Activity', description: 'System monitoring logs' }, + { name: 'Other', description: 'Logs not covered in other categories' }, +]; + +export const logTypeCategoryOptions: any[] = logTypeCategoryInfo.map(({ name, description }) => ({ + value: name, + inputDisplay: name, + dropdownDisplay: ( + <> + {name} + +

{description}

+
+ + ), +})); + +export const logTypesByCategories: { [category: string]: LogType[] } = {}; diff --git a/public/utils/helpers.tsx b/public/utils/helpers.tsx index db37fbe99..c194c3649 100644 --- a/public/utils/helpers.tsx +++ b/public/utils/helpers.tsx @@ -17,7 +17,7 @@ import { import moment from 'moment'; import { PeriodSchedule } from '../../models/interfaces'; import React from 'react'; -import { DEFAULT_EMPTY_DATA, scheduleUnitText } from './constants'; +import { DEFAULT_EMPTY_DATA, logTypesByCategories, scheduleUnitText } from './constants'; import { RuleItem, RuleItemInfo, @@ -31,6 +31,9 @@ import { OpenSearchService } from '../services'; import { ruleSeverity, ruleTypes } from '../pages/Rules/utils/constants'; import { Handler } from 'vega-tooltip'; import _ from 'lodash'; +import { LogType } from '../../types'; +import { DataStore } from '../store/DataStore'; +import { LogCategoryOptionView } from '../components/Utility/LogCategoryOption'; export const parseStringsToOptions = (strings: string[]) => { return strings.map((str) => ({ id: str, label: str })); @@ -295,10 +298,15 @@ export const getPlugins = async (opensearchService: OpenSearchService) => { }; export const formatRuleType = (matchingRuleType: string) => { - return ( - ruleTypes.find((ruleType) => ruleType.label.toLowerCase() === matchingRuleType.toLowerCase()) - ?.label || DEFAULT_EMPTY_DATA + const logType = ruleTypes.find( + (ruleType) => ruleType.label.toLowerCase() === matchingRuleType.toLowerCase() ); + + if (logType) { + return `${logType.category}: ${_.capitalize(logType.label)}`; + } + + return DEFAULT_EMPTY_DATA; }; export const getSeverityBadge = (severity: string) => { @@ -309,3 +317,61 @@ export const getSeverityBadge = (severity: string) => { ); }; + +export function formatToLogTypeOptions(logTypesByCategories: { [category: string]: LogType[] }) { + return Object.entries(logTypesByCategories) + .map(([category, logTypes]) => { + return { + label: category, + value: category, + options: logTypes + .map(({ name }) => ({ + label: name, + value: name.toLowerCase(), + })) + .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)), + }; + }) + .sort((a, b) => { + if (a.label === 'Other') { + return 1; + } else if (b.label === 'Other') { + return -1; + } else { + return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; + } + }); +} + +export async function getLogTypeOptions() { + await DataStore.logTypes.getLogTypes(); + return formatToLogTypeOptions(logTypesByCategories); +} + +export function getLogTypeFilterOptions() { + const options: any[] = []; + formatToLogTypeOptions(logTypesByCategories).forEach((categoryData) => { + const categoryName = categoryData.label; + const logTypes = categoryData.options; + + for (let i = 0; i < logTypes.length; i++) { + if (i === 0) { + options.push({ + value: logTypes.map((logType) => logType.value).join(' or '), + view: , + }); + } + + options.push({ + value: logTypes[i].value, + view: ( + + {_.capitalize(logTypes[i].label)} + + ), + }); + } + }); + + return options; +} diff --git a/types/LogTypes.ts b/types/LogTypes.ts index c831bbe08..52827876f 100644 --- a/types/LogTypes.ts +++ b/types/LogTypes.ts @@ -21,6 +21,7 @@ export interface LogTypeBase { name: string; description: string; source: string; + category: string; tags: { correlation_id: number; } | null;