mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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) )  ## 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 : <img width="1474" alt="Screenshot 2025-02-17 at 14 19 23" src="https://github.com/user-attachments/assets/a8459c71-3208-480e-8049-05293a0a3d2a" /> ### 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>
This commit is contained in:
parent
6507cc3fd0
commit
74ef9fcdee
22 changed files with 521 additions and 35 deletions
|
@ -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<Severity, number> = {
|
||||
low: RISK_SCORE_LOW,
|
||||
medium: RISK_SCORE_MEDIUM,
|
||||
high: RISK_SCORE_HIGH,
|
||||
critical: RISK_SCORE_CRITICAL,
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<Severity, number> = {
|
||||
low: RISK_SCORE_LOW,
|
||||
medium: RISK_SCORE_MEDIUM,
|
||||
high: RISK_SCORE_HIGH,
|
||||
critical: RISK_SCORE_CRITICAL,
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,7 +16,7 @@ export const DATA_INPUT_FILE_UPLOAD_BUTTON = i18n.translate(
|
|||
|
||||
export const UploadFileButton = React.memo<PropsForButton<EuiButtonProps>>((props) => {
|
||||
return (
|
||||
<EuiButton color="success" {...props}>
|
||||
<EuiButton data-test-subj="uploadFileButton" color="success" {...props}>
|
||||
{DATA_INPUT_FILE_UPLOAD_BUTTON}
|
||||
</EuiButton>
|
||||
);
|
||||
|
|
|
@ -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<RulesFileUploadProps> = {}) => {
|
||||
const finalProps = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
render(
|
||||
<I18nProvider>
|
||||
<RulesFileUpload {...finalProps} />
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -138,6 +138,7 @@ const formatRuleRow = (row: SplunkRow<SplunkRulesResult>): 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']) {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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<SplunkSeverity, Severity> = {
|
||||
'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];
|
|
@ -26,6 +26,7 @@ export const ruleMigrationsFieldMap: FieldMap<SchemaFieldMapKeys<Omit<RuleMigrat
|
|||
'original_rule.query_language': { type: 'keyword', required: true },
|
||||
'original_rule.annotations': { type: 'object', required: false },
|
||||
'original_rule.annotations.mitre_attack': { type: 'keyword', array: true, required: false },
|
||||
'original_rule.severity': { type: 'keyword', required: false },
|
||||
elastic_rule: { type: 'object', required: false },
|
||||
'elastic_rule.title': { type: 'text', required: true, fields: { keyword: { type: 'keyword' } } },
|
||||
'elastic_rule.integration_ids': { type: 'keyword', required: false, array: true },
|
||||
|
|
|
@ -7,17 +7,17 @@
|
|||
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import { JsonOutputParser } from '@langchain/core/output_parsers';
|
||||
import {
|
||||
DEFAULT_TRANSLATION_RISK_SCORE,
|
||||
DEFAULT_TRANSLATION_SEVERITY,
|
||||
RuleTranslationResult,
|
||||
} from '../../../../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
|
||||
import type { RuleMigrationsRetriever } from '../../../retrievers';
|
||||
import type { SiemMigrationTelemetryClient } from '../../../rule_migrations_telemetry_client';
|
||||
import type { ChatModel } from '../../../util/actions_client_chat';
|
||||
import { cleanMarkdown, generateAssistantComment } from '../../../util/comments';
|
||||
import type { GraphNode } from '../../types';
|
||||
import { MATCH_PREBUILT_RULE_PROMPT } from './prompts';
|
||||
import {
|
||||
DEFAULT_TRANSLATION_RISK_SCORE,
|
||||
DEFAULT_TRANSLATION_SEVERITY,
|
||||
} from '../../../../constants';
|
||||
|
||||
interface GetMatchPrebuiltRuleNodeParams {
|
||||
model: ChatModel;
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 { SplunkSeverity } from '../../../../../../types';
|
||||
import type { OriginalRule } from '../../../../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import {
|
||||
getElasticRiskScoreFromOriginalRule,
|
||||
getElasticSeverityFromOriginalRule,
|
||||
mapSplunkSeverityToElasticSeverity,
|
||||
} from './severity';
|
||||
|
||||
const defaultSplunkRule: OriginalRule = {
|
||||
id: 'some_id',
|
||||
vendor: 'splunk',
|
||||
title: 'Sample Alert in Essentials',
|
||||
description: '',
|
||||
query: 'source="tutorialdata.zip:*"',
|
||||
query_language: 'spl',
|
||||
severity: '3',
|
||||
};
|
||||
|
||||
describe('tests', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue