From 74ef9fcdee2086bf2c48a35c5c15fb0997fc41dd Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Fri, 21 Feb 2025 17:24:44 +0100 Subject: [PATCH] [Security Solution] - Feat Add `Severity` and `risk_score` to the Siem migrations (#211202) ## Summary Handles below Features: - https://github.com/elastic/security-team/issues/11837 This PR adds `risk_score` and `severity` based on below 3 rules - `Rule Severity` should be mapped to Splunk's `alert.severity`. - `Rule Severity` values should be mapped as mentioned in below section Mapping Elastic Security Rule's Severity with Splunk's Severity > > |Splunk's Severity| Elastic Rule Severity | > |---|---| > |1- Info|Low| > |2-Low|Low| > |3-Medium|Medium| > |4-High|High| > |5-Critical|Critical| - Elastic Security Rule's `Risk Score` derived from the `Severity` of the Rulet based on below mapping( [Source](https://www.elastic.co/guide/en/security/current/rules-ui-create.html#rule-ui-basic-params) ) ![Image](https://github.com/user-attachments/assets/d88acd1f-9b73-467f-bf16-0dbecadec465) ## Desk Testing [splunk_rules_test_severity.json](https://github.com/user-attachments/files/18825855/splunk_rules_test_severity.json) 1. Use the above attached test file which has the `alert.severity` exported from Splunk. 2. Check the Severity of the translated rule should match the mapping given above. Expect results like below : Screenshot 2025-02-17 at 14 19 23 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../common/detection_engine/constants.ts | 14 +- .../common/siem_migrations/constants.ts | 5 - .../model/rule_migration.gen.ts | 4 + .../model/rule_migration.schema.yaml | 3 + .../public/common/constants.ts | 5 - .../components/step_about_rule/data.tsx | 13 -- .../components/step_about_rule/index.tsx | 2 +- .../lib/chart_palette/index.test.ts | 2 +- .../alerts_treemap/lib/chart_palette/index.ts | 2 +- .../components/data_input_flyout/constants.ts | 1 + .../steps/common/upload_file_button.tsx | 2 +- .../rules_file_upload.test.tsx | 208 ++++++++++++++++++ .../rules_file_upload/rules_file_upload.tsx | 1 + .../splunk_rules.test.data.ts | 66 ++++++ .../lib/siem_migrations/rules/constants.ts | 25 +++ .../rules/data/rule_migrations_field_maps.ts | 1 + .../match_prebuilt_rule.ts | 10 +- .../nodes/translate_rule/severity.test.ts | 127 +++++++++++ .../nodes/translate_rule/severity.ts | 40 ++++ .../nodes/translate_rule/translate_rule.ts | 6 + .../translation_result/translation_result.ts | 4 +- .../server/lib/siem_migrations/rules/types.ts | 15 ++ 22 files changed, 521 insertions(+), 35 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/splunk_rules.test.data.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/constants.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/severity.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/severity.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/constants.ts index 270af1a91cf46..e3acea3b1d613 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { Severity, Type } from '@kbn/securitysolution-io-ts-alerting-types'; export enum RULE_PREVIEW_INVOCATION_COUNT { HOUR = 12, @@ -60,3 +60,15 @@ export const SUPPRESSIBLE_ALERT_RULES_GA: Type[] = [ 'threat_match', 'machine_learning', ]; + +export const RISK_SCORE_LOW = 21; +export const RISK_SCORE_MEDIUM = 47; +export const RISK_SCORE_HIGH = 73; +export const RISK_SCORE_CRITICAL = 99; + +export const defaultRiskScoreBySeverity: Record = { + low: RISK_SCORE_LOW, + medium: RISK_SCORE_MEDIUM, + high: RISK_SCORE_HIGH, + critical: RISK_SCORE_CRITICAL, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts index e04ca9a2ae2aa..a717c465618c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; - export const SIEM_MIGRATIONS_ASSISTANT_USER = 'assistant'; export const SIEM_MIGRATIONS_PATH = '/internal/siem_migrations' as const; @@ -61,9 +59,6 @@ export enum RuleTranslationResult { UNTRANSLATABLE = 'untranslatable', } -export const DEFAULT_TRANSLATION_RISK_SCORE = 21; -export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low'; - export const DEFAULT_TRANSLATION_FIELDS = { from: 'now-360s', to: 'now', diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index f7195ef9b14c4..3ce88d0d90184 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -71,6 +71,10 @@ export const OriginalRule = z.object({ * The original rule annotations containing additional information. */ annotations: OriginalRuleAnnotations.optional(), + /** + * The original rule's severity or some representation of it. + */ + severity: z.string().optional(), }); /** diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 7c5e44236f053..a2bed4aa15642 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -56,6 +56,9 @@ components: annotations: description: The original rule annotations containing additional information. $ref: '#/components/schemas/OriginalRuleAnnotations' + severity: + type: string + description: The original rule's severity or some representation of it. ElasticRule: type: object diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/common/constants.ts index 3b5cd00f25a0c..4fc5a7eba2246 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/constants.ts @@ -22,11 +22,6 @@ export const RISK_COLOR_HIGH = euiThemeVars.euiColorVis9_behindText; */ export const RISK_COLOR_CRITICAL = euiThemeVars.euiColorDanger; -export const RISK_SCORE_LOW = 21; -export const RISK_SCORE_MEDIUM = 47; -export const RISK_SCORE_HIGH = 73; -export const RISK_SCORE_CRITICAL = 99; - export const ONBOARDING_VIDEO_SOURCE = '//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html?'; export const DEFAULT_HISTORY_WINDOW_SIZE = '7d'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/data.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/data.tsx index 57e824d7d400d..2458782bc1a7d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/data.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/data.tsx @@ -12,12 +12,6 @@ import { EuiHealth, useEuiTheme } from '@elastic/eui'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import * as I18n from './translations'; -import { - RISK_SCORE_LOW, - RISK_SCORE_MEDIUM, - RISK_SCORE_HIGH, - RISK_SCORE_CRITICAL, -} from '../../../../common/constants'; import { getRiskSeverityColors } from '../../../../common/utils/risk_color_palette'; export interface SeverityOptionItem { @@ -64,10 +58,3 @@ export const useSeverityOptions = () => { return severityOptions; }; - -export const defaultRiskScoreBySeverity: Record = { - low: RISK_SCORE_LOW, - medium: RISK_SCORE_MEDIUM, - high: RISK_SCORE_HIGH, - critical: RISK_SCORE_CRITICAL, -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx index 3accb1dce4396..8e8a65286991c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx @@ -13,6 +13,7 @@ import styled from 'styled-components'; import type { DataViewBase } from '@kbn/es-query'; import type { Severity, Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { defaultRiskScoreBySeverity } from '../../../../../common/detection_engine/constants'; import type { RuleSource } from '../../../../../common/api/detection_engine'; import { isThreatMatchRule, isEsqlRule } from '../../../../../common/detection_engine/utils'; import type { @@ -25,7 +26,6 @@ import { AddMitreAttackThreat } from '../mitre'; import type { FieldHook, FormHook } from '../../../../shared_imports'; import { Field, Form, getUseField, UseField } from '../../../../shared_imports'; -import { defaultRiskScoreBySeverity } from './data'; import { isUrlInvalid } from '../../../../common/utils/validators'; import { schema as defaultSchema } from './schema'; import * as I18n from './translations'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/chart_palette/index.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/chart_palette/index.test.ts index 7780f0a056b39..2c1e6adb86b44 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/chart_palette/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/chart_palette/index.test.ts @@ -10,7 +10,7 @@ import { RISK_SCORE_MEDIUM, RISK_SCORE_HIGH, RISK_SCORE_CRITICAL, -} from '../../../../../../../common/constants'; +} from '../../../../../../../../common/detection_engine/constants'; import { getFillColor, getRiskScorePalette, RISK_SCORE_STEPS } from '.'; import { renderHook } from '@testing-library/react'; import { getRiskSeverityColors } from '../../../../../../../common/utils/risk_color_palette'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/chart_palette/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/chart_palette/index.ts index fdbeb989632e3..9a12d88d8cd2a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/chart_palette/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/chart_palette/index.ts @@ -12,7 +12,7 @@ import { RISK_SCORE_MEDIUM, RISK_SCORE_HIGH, RISK_SCORE_CRITICAL, -} from '../../../../../../../common/constants'; +} from '../../../../../../../../common/detection_engine/constants'; import { getRiskSeverityColors } from '../../../../../../../common/utils/risk_color_palette'; /** diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts index d390b395ed98f..2544f8acc1e42 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts @@ -12,6 +12,7 @@ export const SPLUNK_RULES_COLUMNS = [ 'description', 'action.escu.eli5', 'action.correlationsearch.annotations', + 'alert.severity', ] as const; export const RULES_SPLUNK_QUERY = `| rest /servicesNS/-/-/saved/searches diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/upload_file_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/upload_file_button.tsx index 1be66ac0a1e76..e76b2810049c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/upload_file_button.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/upload_file_button.tsx @@ -16,7 +16,7 @@ export const DATA_INPUT_FILE_UPLOAD_BUTTON = i18n.translate( export const UploadFileButton = React.memo>((props) => { return ( - + {DATA_INPUT_FILE_UPLOAD_BUTTON} ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.test.tsx new file mode 100644 index 0000000000000..f3ba48b3efea0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.test.tsx @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import type { RulesFileUploadProps } from './rules_file_upload'; +import { RulesFileUpload } from './rules_file_upload'; +import type { CreateMigration } from '../../../../../../service/hooks/use_create_migration'; +import { screen } from '@elastic/eui/lib/test/rtl'; +import { I18nProvider } from '@kbn/i18n-react'; +// eslint-disable-next-line import/no-nodejs-modules +import path from 'path'; +// eslint-disable-next-line import/no-nodejs-modules +import os from 'os'; +import { splunkTestRules } from './splunk_rules.test.data'; +import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; + +const mockCreateMigration: CreateMigration = jest.fn(); +const mockApiError = 'Some Mock API Error'; + +const defaultProps: RulesFileUploadProps = { + createMigration: mockCreateMigration, + apiError: undefined, + isLoading: false, + isCreated: false, +}; + +const renderTestComponent = (props: Partial = {}) => { + const finalProps = { + ...defaultProps, + ...props, + }; + render( + + + + ); +}; + +const getTestDir = () => os.tmpdir(); + +const createRulesFileFromRulesData = ( + data: string, + destinationDirectory: string, + fileName: string +) => { + const filePath = path.join(destinationDirectory, fileName); + const file = new File([data], filePath, { + type: 'application/x-ndjson', + }); + return file; +}; + +describe('RulesFileUpload', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the upload button', () => { + renderTestComponent(); + + expect(screen.getByTestId('rulesFilePicker')).toBeInTheDocument(); + expect(screen.getByTestId('uploadFileButton')).toBeDisabled(); + }); + + it('should be able to upload correct file type', async () => { + const fileName = 'splunk_rules.test.data.json'; + const ndJSONString = splunkTestRules.map((obj) => JSON.stringify(obj)).join('\n'); + const testFile = createRulesFileFromRulesData(ndJSONString, getTestDir(), fileName); + + renderTestComponent(); + + const filePicker = screen.getByTestId('rulesFilePicker'); + + act(() => { + fireEvent.change(filePicker, { + target: { + files: [testFile], + }, + }); + }); + + await waitFor(() => { + expect(filePicker).toHaveAttribute('data-loading', 'true'); + }); + + await waitFor(() => { + expect(filePicker).toHaveAttribute('data-loading', 'false'); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('uploadFileButton')); + }); + + await waitFor(() => { + expect(mockCreateMigration).toHaveBeenCalled(); + }); + + const rulesToExpect: OriginalRule[] = splunkTestRules.map(({ result: rule }) => ({ + id: rule.id, + vendor: 'splunk', + title: rule.title, + query: rule.search, + query_language: 'spl', + description: rule.description, + severity: rule['alert.severity'] as OriginalRule['severity'], + })); + + expect(mockCreateMigration).toHaveBeenNthCalledWith(1, rulesToExpect); + }); + + describe('Error Handling', () => { + const scenarios = [ + { + subject: 'Non Object Entries', + fileContent: '["asdadsada"]', + errorMessage: 'The file contains non-object entries', + }, + { + subject: 'Non parsable JSON or ND-JSON', + fileContent: '[{"testArray"}])', + errorMessage: 'Cannot parse the file as either a JSON file or NDJSON file', + }, + { + subject: 'Empty File', + fileContent: '', + errorMessage: 'The file is empty', + }, + ]; + + it('should not be able to upload on API Error', async () => { + renderTestComponent({ + apiError: mockApiError, + }); + + const fileName = 'splunk_rules.test.data.json'; + const ndJSONString = splunkTestRules.map((obj) => JSON.stringify(obj)).join('\n'); + const testFile = createRulesFileFromRulesData(ndJSONString, getTestDir(), fileName); + + const filePicker = screen.getByTestId('rulesFilePicker'); + + act(() => { + fireEvent.change(filePicker, { + target: { + files: [testFile], + }, + }); + }); + + await waitFor(() => { + expect(filePicker).toHaveAttribute('data-loading', 'true'); + }); + + await waitFor(() => { + expect(filePicker).toHaveAttribute('data-loading', 'false'); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('uploadFileButton')); + }); + + await waitFor(() => { + expect(screen.getByText(mockApiError)).toBeVisible(); + expect(screen.getByTestId('uploadFileButton')).toBeDisabled(); + }); + }); + scenarios.forEach((scenario, _idx) => { + it(`should not be able to upload when file has - ${scenario.subject}`, async () => { + const fileName = 'invalid_rule_file.json'; + const testFile = createRulesFileFromRulesData(scenario.fileContent, getTestDir(), fileName); + + renderTestComponent({ + apiError: undefined, + }); + + const filePicker = screen.getByTestId('rulesFilePicker'); + + act(() => { + fireEvent.change(filePicker, { + target: { + files: [testFile], + }, + }); + }); + + await waitFor(() => { + expect(filePicker).toHaveAttribute('data-loading', 'true'); + }); + + await waitFor(() => { + expect(filePicker).toHaveAttribute('data-loading', 'false'); + }); + + await waitFor(() => { + expect(screen.getByText(scenario.errorMessage)).toBeVisible(); + }); + + await waitFor(() => { + expect(screen.getByTestId('uploadFileButton')).toBeDisabled(); + }); + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx index dfe11234f726b..f8a70d0bf1365 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx @@ -138,6 +138,7 @@ const formatRuleRow = (row: SplunkRow): OriginalRule => { query: row.result.search, query_language: 'spl', description: row.result['action.escu.eli5']?.trim() || row.result.description, + severity: row.result['alert.severity'] as OriginalRule['severity'], }; if (row.result['action.correlationsearch.annotations']) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/splunk_rules.test.data.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/splunk_rules.test.data.ts new file mode 100644 index 0000000000000..0d40d903d96d0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/splunk_rules.test.data.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const splunkTestRules = [ + { + preview: false, + result: { + id: 'some_id1', + title: 'Alert with IP Method and URI Filters with Default Severity', + search: + 'source="testing_data.zip:*" clientip="198.35.1.75" method=POST uri_path="/cart/error.do"', + description: '', + 'alert.severity': '3', + }, + }, + { + preview: false, + result: { + id: 'some_id2', + title: 'New Alert with Index filter', + search: 'source="testing_data.zip:*" | search server="MacBookPro.fritz.box" index=main', + description: 'Tutorial data based on host name', + 'alert.severity': '5', + }, + }, + { + preview: false, + result: { + id: 'some_id3', + title: 'Sample Alert in Essentials', + search: 'source="testing_file.zip:*"', + description: '', + 'alert.severity': '3', + }, + }, + { + preview: false, + lastrow: true, + result: { + id: 'some_id4', + title: 'Tutorial data based on host name', + search: 'source="testing_file.zip:*" \n| search host=vendor_sales', + description: 'Tutorial data based on host name', + 'alert.severity': '5', + }, + }, +]; + +export const invalidTestRulesFormat = [ + { + result: { + some: 'key', + }, + }, + { + result: { + description: { + some: 'key', + }, + }, + }, +]; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/constants.ts new file mode 100644 index 0000000000000..8ede8e1c610bd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/constants.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; +import { defaultRiskScoreBySeverity } from '../../../../common/detection_engine/constants'; +import type { SplunkSeverity } from './types'; + +export const SPLUNK_ELASTIC_ALERT_SEVERITY_MAP: Record = { + '1': 'low', + '2': 'low', + '3': 'medium', + '4': 'high', + '5': 'critical', +} as const; + +export const ELASTIC_SEVERITY_TO_RISK_SCORE_MAP = defaultRiskScoreBySeverity; + +export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low'; + +export const DEFAULT_TRANSLATION_RISK_SCORE = + ELASTIC_SEVERITY_TO_RISK_SCORE_MAP[DEFAULT_TRANSLATION_SEVERITY]; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index 491f14597220c..0bac373877cef 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -26,6 +26,7 @@ export const ruleMigrationsFieldMap: FieldMap { + describe('getElasticRiskScoreFromOriginalRule', () => { + describe('splunk', () => { + describe('when there is a vendor match', () => { + it('should return the correct risk score', () => { + const riskScore = getElasticRiskScoreFromOriginalRule(defaultSplunkRule); + expect(riskScore).toEqual(47); + }); + }); + describe('when there is no vendor match', () => { + it('should return default risk score', () => { + expect( + getElasticRiskScoreFromOriginalRule({ + ...defaultSplunkRule, + /* @ts-expect-error because vendor type is "splunk" which raises error below */ + vendor: 'not_splunk', + query_language: 'not_spl', + }) + ).toEqual(21); + }); + }); + }); + }); + + describe('mapSplunkSeverityToElasticSeverity', () => { + describe('when there is a match', () => { + const tests: Array<{ + input: SplunkSeverity; + expected: string; + }> = [ + { + input: '1', + expected: 'low', + }, + { + input: '2', + expected: 'low', + }, + { + input: '3', + expected: 'medium', + }, + { + input: '4', + expected: 'high', + }, + { + input: '5', + expected: 'critical', + }, + ]; + + tests.forEach((test) => { + it(`should maps severity ${test.input} to ${test.expected}`, () => { + expect(mapSplunkSeverityToElasticSeverity(test.input)).toEqual(test.expected); + }); + }); + }); + describe('when there is no match', () => { + it('should return default severity when there is no match', () => { + expect( + mapSplunkSeverityToElasticSeverity('an_invalid_severity' as unknown as SplunkSeverity) + ).toEqual('low'); + }); + + it('should return default severity when there is no severity', () => { + expect(mapSplunkSeverityToElasticSeverity()).toEqual('low'); + }); + }); + }); + + describe('getElasticSeverityFromOriginalRule', () => { + describe('splunk', () => { + describe('when there is a vendor match', () => { + it('should call the correct function with the correct severity', () => { + expect(getElasticSeverityFromOriginalRule(defaultSplunkRule)).toBe('medium'); + }); + }); + describe('when there is no vendor match', () => { + it('should return default severity when there is no match', () => { + expect( + getElasticSeverityFromOriginalRule({ + ...defaultSplunkRule, + /* @ts-expect-error because vendor type is "splunk" which raises error below */ + vendor: undefined, + query_language: 'not_spl', + }) + ).toEqual('low'); + }); + + it('should return default severity when there is no severity', () => { + expect( + getElasticSeverityFromOriginalRule({ + ...defaultSplunkRule, + severity: undefined, + }) + ).toBe('low'); + }); + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/severity.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/severity.ts new file mode 100644 index 0000000000000..b09577a5f2afe --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/severity.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { OriginalRule } from '../../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { SplunkSeverity } from '../../../../../../types'; +import { + DEFAULT_TRANSLATION_SEVERITY, + ELASTIC_SEVERITY_TO_RISK_SCORE_MAP, + SPLUNK_ELASTIC_ALERT_SEVERITY_MAP, +} from '../../../../../../constants'; + +export const mapSplunkSeverityToElasticSeverity = (splunkSeverity?: SplunkSeverity): Severity => { + if (!splunkSeverity) { + return DEFAULT_TRANSLATION_SEVERITY; + } + return SPLUNK_ELASTIC_ALERT_SEVERITY_MAP[splunkSeverity] || DEFAULT_TRANSLATION_SEVERITY; +}; + +export const getElasticSeverityFromOriginalRule = (originalRule: OriginalRule) => { + return originalRule.query_language === 'spl' || originalRule.vendor === 'splunk' + ? mapSplunkSeverityToElasticSeverity(originalRule.severity as SplunkSeverity) + : DEFAULT_TRANSLATION_SEVERITY; +}; + +export const getElasticRiskScoreFromElasticSeverity = (elasticSeverity: Severity) => { + return ELASTIC_SEVERITY_TO_RISK_SCORE_MAP[elasticSeverity]; +}; + +export const getElasticRiskScoreFromOriginalRule = (originalRule: OriginalRule) => { + if (originalRule.vendor === 'splunk') { + const elasticSeverity = getElasticSeverityFromOriginalRule(originalRule); + return getElasticRiskScoreFromElasticSeverity(elasticSeverity); + } + return ELASTIC_SEVERITY_TO_RISK_SCORE_MAP[DEFAULT_TRANSLATION_SEVERITY]; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts index e25a0378f6499..7be5e96d84bb8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts @@ -10,6 +10,10 @@ import { cleanMarkdown, generateAssistantComment } from '../../../../../util/com import type { EsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base'; import type { GraphNode } from '../../types'; import { ESQL_SYNTAX_TRANSLATION_PROMPT } from './prompts'; +import { + getElasticRiskScoreFromOriginalRule, + getElasticSeverityFromOriginalRule, +} from './severity'; interface GetTranslateRuleNodeParams { esqlKnowledgeBase: EsqlKnowledgeBase; @@ -48,6 +52,8 @@ export const getTranslateRuleNode = ({ integration_ids: [integrationId], query: esqlQuery, query_language: 'esql', + risk_score: getElasticRiskScoreFromOriginalRule(state.original_rule), + severity: getElasticSeverityFromOriginalRule(state.original_rule), }, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translation_result/translation_result.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translation_result/translation_result.ts index b38dd3ed45ae3..2ee45c9f9219e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translation_result/translation_result.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translation_result/translation_result.ts @@ -8,8 +8,8 @@ import { DEFAULT_TRANSLATION_RISK_SCORE, DEFAULT_TRANSLATION_SEVERITY, - RuleTranslationResult, -} from '../../../../../../../../../../common/siem_migrations/constants'; +} from '../../../../../../constants'; +import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; import type { GraphNode } from '../../types'; export const getTranslationResultNode = (): GraphNode => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/types.ts index 521c03a9e38c4..cb01f39978c7c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/types.ts @@ -56,3 +56,18 @@ export type RuleSemanticSearchResult = RuleMigrationPrebuiltRule & RuleVersions; export type InternalUpdateRuleMigrationData = UpdateRuleMigrationData & { translation_result?: RuleMigrationTranslationResult; }; + +/** + * + * Based on the severity levels defined in the Splunk Common Information Model (CIM) documentation + * + * https://docs.splunk.com/Documentation/CIM/6.0.2/User/Alerts + * + * '1': 'INFO'; + * '2': 'LOW'; + * '3': 'MEDIUM'; + * '4': 'HIGH'; + * '5': 'CRITICAL'; + * + **/ +export type SplunkSeverity = '1' | '2' | '3' | '4' | '5';