Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.0] [Security Solution] - Feat Add Severity and risk_score to the Siem migrations (#211202) #212119

Open
wants to merge 1 commit into
base: 9.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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']) {
Expand Down
Loading