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

[Security Solution][Detections] Rule Preview should process override fields and exceptions (#4680) #140221

Merged
merged 12 commits into from
Sep 13, 2022
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ export const RISK_OVERRIDE =

export const RULES_CREATION_FORM = '[data-test-subj="stepDefineRule"]';

export const RULES_CREATION_PREVIEW = '[data-test-subj="rule-preview"]';
export const RULES_CREATION_PREVIEW_BUTTON = '[data-test-subj="preview-flyout"]';

export const RULES_CREATION_PREVIEW_REFRESH_BUTTON = '[data-test-subj="previewSubmitButton"]';

export const RULE_DESCRIPTION_INPUT =
'[data-test-subj="detectionEngineStepAboutRuleDescription"] [data-test-subj="input"]';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ import {
RULE_STATUS,
RULE_TIMESTAMP_OVERRIDE,
RULES_CREATION_FORM,
RULES_CREATION_PREVIEW,
RULES_CREATION_PREVIEW_BUTTON,
RULES_CREATION_PREVIEW_REFRESH_BUTTON,
RUNS_EVERY_INTERVAL,
RUNS_EVERY_TIME_TYPE,
SCHEDULE_CONTINUE_BUTTON,
Expand Down Expand Up @@ -336,15 +337,13 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => {
cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).should('be.visible');
cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).type(rule.customQuery);
cy.get(RULES_CREATION_FORM).find(EQL_QUERY_VALIDATION_SPINNER).should('not.exist');
cy.get(RULES_CREATION_PREVIEW)
.find(QUERY_PREVIEW_BUTTON)
.should('not.be.disabled')
.click({ force: true });
cy.get(RULES_CREATION_PREVIEW_BUTTON).should('not.be.disabled').click({ force: true });
cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).should('not.be.disabled').click({ force: true });
cy.get(PREVIEW_HISTOGRAM)
.invoke('text')
.then((text) => {
if (text !== 'Rule Preview') {
cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true });
cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).click({ force: true });
cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Rule Preview');
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import moment from 'moment';
import { DataSourceType } from '../../../pages/detection_engine/rules/types';
import {
isNoisy,
Expand All @@ -14,51 +15,75 @@ import {
} from './helpers';

describe('query_preview/helpers', () => {
const timeframeEnd = moment();
const startHourAgo = timeframeEnd.clone().subtract(1, 'hour');
const startDayAgo = timeframeEnd.clone().subtract(1, 'day');
const startMonthAgo = timeframeEnd.clone().subtract(1, 'month');

const lastHourTimeframe = {
timeframeStart: startHourAgo,
timeframeEnd,
interval: '5m',
lookback: '1m',
};
const lastDayTimeframe = {
timeframeStart: startDayAgo,
timeframeEnd,
interval: '1h',
lookback: '5m',
};
const lastMonthTimeframe = {
timeframeStart: startMonthAgo,
timeframeEnd,
interval: '1d',
lookback: '1h',
};

describe('isNoisy', () => {
test('returns true if timeframe selection is "Last hour" and average hits per hour is greater than one execution duration', () => {
const isItNoisy = isNoisy(30, 'h');
const isItNoisy = isNoisy(30, lastHourTimeframe);

expect(isItNoisy).toBeTruthy();
});

test('returns false if timeframe selection is "Last hour" and average hits per hour is less than one execution duration', () => {
const isItNoisy = isNoisy(0, 'h');
const isItNoisy = isNoisy(0, lastHourTimeframe);

expect(isItNoisy).toBeFalsy();
});

test('returns true if timeframe selection is "Last day" and average hits per hour is greater than one execution duration', () => {
const isItNoisy = isNoisy(50, 'd');
const isItNoisy = isNoisy(50, lastDayTimeframe);

expect(isItNoisy).toBeTruthy();
});

test('returns false if timeframe selection is "Last day" and average hits per hour is equal to one execution duration', () => {
const isItNoisy = isNoisy(24, 'd');
const isItNoisy = isNoisy(24, lastDayTimeframe);

expect(isItNoisy).toBeFalsy();
});

test('returns false if timeframe selection is "Last day" and hits is 0', () => {
const isItNoisy = isNoisy(0, 'd');
const isItNoisy = isNoisy(0, lastDayTimeframe);

expect(isItNoisy).toBeFalsy();
});

test('returns true if timeframe selection is "Last month" and average hits per hour is greater than one execution duration', () => {
const isItNoisy = isNoisy(50, 'M');
const isItNoisy = isNoisy(750, lastMonthTimeframe);

expect(isItNoisy).toBeTruthy();
});

test('returns false if timeframe selection is "Last month" and average hits per hour is equal to one execution duration', () => {
const isItNoisy = isNoisy(30, 'M');
const isItNoisy = isNoisy(30, lastMonthTimeframe);

expect(isItNoisy).toBeFalsy();
});

test('returns false if timeframe selection is "Last month" and hits is 0', () => {
const isItNoisy = isNoisy(0, 'M');
const isItNoisy = isNoisy(0, lastMonthTimeframe);

expect(isItNoisy).toBeFalsy();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { isEmpty } from 'lodash';
import { Position, ScaleType } from '@elastic/charts';
import type { EuiSelectOption } from '@elastic/eui';
import type { Type, Language, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import type { Unit } from '@kbn/datemath';
import type { Filter } from '@kbn/es-query';
import * as i18n from './translations';
import { histogramDateTimeFormatter } from '../../../../common/components/utils';
import type { ChartSeriesConfigs } from '../../../../common/components/charts/common';
import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter';
import type { FieldValueQueryBar } from '../query_bar';
import type { ESQuery } from '../../../../../common/typed_json';
import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types';
import { DataSourceType } from '../../../pages/detection_engine/rules/types';

/**
Expand All @@ -25,18 +25,13 @@ import { DataSourceType } from '../../../pages/detection_engine/rules/types';
* @param hits Total query search hits
* @param timeframe Range selected by user (last hour, day...)
*/
export const isNoisy = (hits: number, timeframe: Unit): boolean => {
if (timeframe === 'h') {
return hits > 1;
} else if (timeframe === 'd') {
return hits / 24 > 1;
} else if (timeframe === 'w') {
return hits / 168 > 1;
} else if (timeframe === 'M') {
return hits / 30 > 1;
}

return false;
export const isNoisy = (hits: number, timeframe: TimeframePreviewOptions): boolean => {
const oneHour = 1000 * 60 * 60;
const durationInHours = Math.max(
(timeframe.timeframeEnd.valueOf() - timeframe.timeframeStart.valueOf()) / oneHour,
1.0
);
return hits / durationInHours > 1;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@

import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import type { DataViewBase } from '@kbn/es-query';
import { fields } from '@kbn/data-plugin/common/mocks';

import { TestProviders } from '../../../../common/mock';
import type { RulePreviewProps } from '.';
import { RulePreview } from '.';
import { RulePreview, REASONABLE_INVOCATION_COUNT } from '.';
import { usePreviewRoute } from './use_preview_route';
import { usePreviewHistogram } from './use_preview_histogram';
import { DataSourceType } from '../../../pages/detection_engine/rules/types';
import {
getStepScheduleDefaultValue,
stepAboutDefaultValue,
stepDefineDefaultValue,
} from '../../../pages/detection_engine/rules/utils';
import { usePreviewInvocationCount } from '../../../containers/detection_engine/rules/use_preview_invocation_count';

jest.mock('../../../../common/lib/kibana');
jest.mock('./use_preview_route');
Expand All @@ -30,6 +35,7 @@ jest.mock('../../../../common/containers/use_global_time', () => ({
setQuery: jest.fn(),
}),
}));
jest.mock('../../../containers/detection_engine/rules/use_preview_invocation_count');

const getMockIndexPattern = (): DataViewBase => ({
fields,
Expand All @@ -38,42 +44,46 @@ const getMockIndexPattern = (): DataViewBase => ({
});

const defaultProps: RulePreviewProps = {
ruleType: 'threat_match',
index: ['test-*'],
indexPattern: getMockIndexPattern(),
dataSourceType: DataSourceType.IndexPatterns,
threatIndex: ['threat-*'],
threatMapping: [
{
entries: [
{ field: 'file.hash.md5', value: 'threat.indicator.file.hash.md5', type: 'mapping' },
],
defineRuleData: {
...stepDefineDefaultValue,
ruleType: 'threat_match',
index: ['test-*'],
indexPattern: getMockIndexPattern(),
dataSourceType: DataSourceType.IndexPatterns,
threatIndex: ['threat-*'],
threatMapping: [
{
entries: [
{ field: 'file.hash.md5', value: 'threat.indicator.file.hash.md5', type: 'mapping' },
],
},
],
queryBar: {
filters: [],
query: { query: 'file.hash.md5:*', language: 'kuery' },
saved_id: null,
},
],
isDisabled: false,
query: {
filters: [],
query: { query: 'file.hash.md5:*', language: 'kuery' },
saved_id: null,
},
threatQuery: {
filters: [],
query: { query: 'threat.indicator.file.hash.md5:*', language: 'kuery' },
saved_id: null,
},
threshold: {
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: '2',
threatQueryBar: {
filters: [],
query: { query: 'threat.indicator.file.hash.md5:*', language: 'kuery' },
saved_id: null,
},
threshold: {
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: '2',
},
},
anomalyThreshold: 50,
machineLearningJobId: ['test-ml-job-id'],
eqlOptions: {},
newTermsFields: ['host.ip'],
historyWindowSize: '7d',
},
anomalyThreshold: 50,
machineLearningJobId: ['test-ml-job-id'],
eqlOptions: {},
newTermsFields: ['host.ip'],
historyWindowSize: '7d',
aboutRuleData: stepAboutDefaultValue,
scheduleRuleData: getStepScheduleDefaultValue('threat_match'),
};

describe('PreviewQuery', () => {
Expand All @@ -98,6 +108,8 @@ describe('PreviewQuery', () => {
isPreviewRequestInProgress: false,
previewId: undefined,
});

(usePreviewInvocationCount as jest.Mock).mockReturnValue({ invocationCount: 500 });
});

afterEach(() => {
Expand All @@ -115,26 +127,6 @@ describe('PreviewQuery', () => {
expect(await wrapper.findByTestId('preview-time-frame')).toBeTruthy();
});

test('it renders preview button disabled if "isDisabled" is true', async () => {
const wrapper = render(
<TestProviders>
<RulePreview {...defaultProps} isDisabled={true} />
</TestProviders>
);

expect(await wrapper.getByTestId('queryPreviewButton').closest('button')).toBeDisabled();
});

test('it renders preview button enabled if "isDisabled" is false', async () => {
const wrapper = render(
<TestProviders>
<RulePreview {...defaultProps} />
</TestProviders>
);

expect(await wrapper.getByTestId('queryPreviewButton').closest('button')).not.toBeDisabled();
});

test('does not render histogram when there is no previewId', async () => {
const wrapper = render(
<TestProviders>
Expand All @@ -145,40 +137,9 @@ describe('PreviewQuery', () => {
expect(await wrapper.queryByTestId('[data-test-subj="preview-histogram-panel"]')).toBeNull();
});

test('it renders quick/advanced query toggle button', async () => {
const wrapper = render(
<TestProviders>
<RulePreview {...defaultProps} />
</TestProviders>
);

expect(await wrapper.findByTestId('quickAdvancedToggleButtonGroup')).toBeTruthy();
});

test('it renders timeframe, interval and look-back buttons when advanced query is selected', async () => {
const wrapper = render(
<TestProviders>
<RulePreview {...defaultProps} />
</TestProviders>
);

expect(await wrapper.findByTestId('quickAdvancedToggleButtonGroup')).toBeTruthy();
const advancedQueryButton = await wrapper.findByTestId('advancedQuery');
userEvent.click(advancedQueryButton);
expect(await wrapper.findByTestId('detectionEnginePreviewRuleInterval')).toBeTruthy();
expect(await wrapper.findByTestId('detectionEnginePreviewRuleLookback')).toBeTruthy();
});

test('it renders invocation count warning when advanced query is selected and warning flag is set to true', async () => {
(usePreviewRoute as jest.Mock).mockReturnValue({
hasNoiseWarning: false,
addNoiseWarning: jest.fn(),
createPreview: jest.fn(),
clearPreview: jest.fn(),
logs: [],
isPreviewRequestInProgress: false,
previewId: undefined,
showInvocationCountWarning: true,
test('it renders invocation count warning when invocation count is bigger then "REASONABLE_INVOCATION_COUNT"', async () => {
(usePreviewInvocationCount as jest.Mock).mockReturnValue({
invocationCount: REASONABLE_INVOCATION_COUNT + 1,
});

const wrapper = render(
Expand All @@ -187,8 +148,6 @@ describe('PreviewQuery', () => {
</TestProviders>
);

const advancedQueryButton = await wrapper.findByTestId('advancedQuery');
userEvent.click(advancedQueryButton);
expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy();
});
});
Loading