mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Switches remaining rule types to use new Rule Preview API (#116374)
This commit is contained in:
parent
894f89d8ce
commit
5d44d79c2b
55 changed files with 614 additions and 2643 deletions
|
@ -32,4 +32,3 @@ export const config: PluginConfigDescriptor = {
|
|||
export type RuleRegistryPluginConfig = TypeOf<typeof config.schema>;
|
||||
|
||||
export const INDEX_PREFIX = '.alerts' as const;
|
||||
export const INDEX_PREFIX_FOR_BACKING_INDICES = '.internal.alerts' as const;
|
||||
|
|
|
@ -29,6 +29,7 @@ export const createRuleDataClientMock = (
|
|||
indexName,
|
||||
kibanaVersion: '7.16.0',
|
||||
isWriteEnabled: jest.fn(() => true),
|
||||
indexNameWithNamespace: jest.fn((namespace: string) => indexName + namespace),
|
||||
|
||||
// @ts-ignore 4.3.5 upgrade
|
||||
getReader: jest.fn((_options?: { namespace?: string }) => ({
|
||||
|
|
|
@ -54,6 +54,10 @@ export class RuleDataClient implements IRuleDataClient {
|
|||
return this.options.indexInfo.kibanaVersion;
|
||||
}
|
||||
|
||||
public indexNameWithNamespace(namespace: string): string {
|
||||
return this.options.indexInfo.getPrimaryAlias(namespace);
|
||||
}
|
||||
|
||||
private get writeEnabled(): boolean {
|
||||
return this._isWriteEnabled;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_fie
|
|||
|
||||
export interface IRuleDataClient {
|
||||
indexName: string;
|
||||
indexNameWithNamespace(namespace: string): string;
|
||||
kibanaVersion: string;
|
||||
isWriteEnabled(): boolean;
|
||||
getReader(options?: { namespace?: string }): IRuleDataReader;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { INDEX_PREFIX, INDEX_PREFIX_FOR_BACKING_INDICES } from '../config';
|
||||
import { INDEX_PREFIX } from '../config';
|
||||
import { IndexOptions } from './index_options';
|
||||
import { joinWithDash } from './utils';
|
||||
|
||||
|
@ -23,16 +23,16 @@ interface ConstructorOptions {
|
|||
export class IndexInfo {
|
||||
constructor(options: ConstructorOptions) {
|
||||
const { indexOptions, kibanaVersion } = options;
|
||||
const { registrationContext, dataset } = indexOptions;
|
||||
const { registrationContext, dataset, additionalPrefix } = indexOptions;
|
||||
|
||||
this.indexOptions = indexOptions;
|
||||
this.kibanaVersion = kibanaVersion;
|
||||
this.baseName = joinWithDash(INDEX_PREFIX, `${registrationContext}.${dataset}`);
|
||||
this.basePattern = joinWithDash(this.baseName, '*');
|
||||
this.baseNameForBackingIndices = joinWithDash(
|
||||
INDEX_PREFIX_FOR_BACKING_INDICES,
|
||||
this.baseName = joinWithDash(
|
||||
`${additionalPrefix ?? ''}${INDEX_PREFIX}`,
|
||||
`${registrationContext}.${dataset}`
|
||||
);
|
||||
this.basePattern = joinWithDash(this.baseName, '*');
|
||||
this.baseNameForBackingIndices = `.internal${this.baseName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -95,6 +95,17 @@ export interface IndexOptions {
|
|||
* @example '.siem-signals', undefined
|
||||
*/
|
||||
secondaryAlias?: string;
|
||||
|
||||
/**
|
||||
* Optional prefix name that will be prepended to indices in addition to
|
||||
* primary dataset and context naming convention.
|
||||
*
|
||||
* Currently used only for creating a preview index for the purpose of
|
||||
* previewing alerts from a rule. The documents are identical to alerts, but
|
||||
* shouldn't exist on an alert index and shouldn't be queried together with
|
||||
* real alerts in any way, because the rule that created them doesn't exist
|
||||
*/
|
||||
additionalPrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -39,7 +39,7 @@ export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults' as const;
|
|||
export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults' as const;
|
||||
export const DEFAULT_ALERTS_INDEX = '.alerts-security.alerts' as const;
|
||||
export const DEFAULT_SIGNALS_INDEX = '.siem-signals' as const;
|
||||
export const DEFAULT_PREVIEW_INDEX = '.siem-preview-signals' as const;
|
||||
export const DEFAULT_PREVIEW_INDEX = '.preview.alerts-security.alerts' as const;
|
||||
export const DEFAULT_LISTS_INDEX = '.lists' as const;
|
||||
export const DEFAULT_ITEMS_INDEX = '.items' as const;
|
||||
// The DEFAULT_MAX_SIGNALS value exists also in `x-pack/plugins/cases/common/constants.ts`
|
||||
|
@ -256,8 +256,6 @@ export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL =
|
|||
export const DETECTION_ENGINE_RULES_BULK_ACTION =
|
||||
`${DETECTION_ENGINE_RULES_URL}/_bulk_action` as const;
|
||||
export const DETECTION_ENGINE_RULES_PREVIEW = `${DETECTION_ENGINE_RULES_URL}/preview` as const;
|
||||
export const DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL =
|
||||
`${DETECTION_ENGINE_RULES_PREVIEW}/index` as const;
|
||||
|
||||
/**
|
||||
* Internal detection engine routes
|
||||
|
|
|
@ -5,11 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { createPreviewIndex } from './api';
|
||||
|
||||
export const usePreviewIndex = () => {
|
||||
useEffect(() => {
|
||||
createPreviewIndex();
|
||||
}, []);
|
||||
};
|
||||
export enum RULE_PREVIEW_INVOCATION_COUNT {
|
||||
HOUR = 20,
|
||||
DAY = 24,
|
||||
WEEK = 168,
|
||||
MONTH = 30,
|
||||
}
|
|
@ -370,6 +370,7 @@ export const previewRulesSchema = t.intersection([
|
|||
createTypeSpecific,
|
||||
t.type({ invocationCount: t.number }),
|
||||
]);
|
||||
export type PreviewRulesSchema = t.TypeOf<typeof previewRulesSchema>;
|
||||
|
||||
type UpdateSchema<T> = SharedUpdateSchema & T;
|
||||
export type EqlUpdateSchema = UpdateSchema<t.TypeOf<typeof eqlCreateParams>>;
|
||||
|
|
|
@ -174,7 +174,7 @@ describe('Detection rules, threshold', () => {
|
|||
cy.get(ALERT_GRID_CELL).contains(rule.name);
|
||||
});
|
||||
|
||||
it('Preview results of keyword using "host.name"', () => {
|
||||
it.skip('Preview results of keyword using "host.name"', () => {
|
||||
rule.index = [...rule.index, '.siem-signals*'];
|
||||
|
||||
createCustomRuleActivated(getNewRule());
|
||||
|
@ -188,7 +188,7 @@ describe('Detection rules, threshold', () => {
|
|||
cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits');
|
||||
});
|
||||
|
||||
it('Preview results of "ip" using "source.ip"', () => {
|
||||
it.skip('Preview results of "ip" using "source.ip"', () => {
|
||||
const previewRule: ThresholdRule = {
|
||||
...rule,
|
||||
thresholdField: 'source.ip',
|
||||
|
|
|
@ -104,9 +104,9 @@ export const DEFINE_INDEX_INPUT =
|
|||
|
||||
export const EQL_TYPE = '[data-test-subj="eqlRuleType"]';
|
||||
|
||||
export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]';
|
||||
export const PREVIEW_HISTOGRAM = '[data-test-subj="preview-histogram-panel"]';
|
||||
|
||||
export const EQL_QUERY_PREVIEW_HISTOGRAM = '[data-test-subj="queryPreviewEqlHistogram"]';
|
||||
export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]';
|
||||
|
||||
export const EQL_QUERY_VALIDATION_SPINNER = '[data-test-subj="eql-validation-loading"]';
|
||||
|
||||
|
@ -170,7 +170,7 @@ export const RISK_OVERRIDE =
|
|||
|
||||
export const RULES_CREATION_FORM = '[data-test-subj="stepDefineRule"]';
|
||||
|
||||
export const RULES_CREATION_PREVIEW = '[data-test-subj="ruleCreationQueryPreview"]';
|
||||
export const RULES_CREATION_PREVIEW = '[data-test-subj="rule-preview"]';
|
||||
|
||||
export const RULE_DESCRIPTION_INPUT =
|
||||
'[data-test-subj="detectionEngineStepAboutRuleDescription"] [data-test-subj="input"]';
|
||||
|
|
|
@ -33,7 +33,6 @@ import {
|
|||
DEFAULT_RISK_SCORE_INPUT,
|
||||
DEFINE_CONTINUE_BUTTON,
|
||||
EQL_QUERY_INPUT,
|
||||
EQL_QUERY_PREVIEW_HISTOGRAM,
|
||||
EQL_QUERY_VALIDATION_SPINNER,
|
||||
EQL_TYPE,
|
||||
FALSE_POSITIVES_INPUT,
|
||||
|
@ -92,6 +91,7 @@ import {
|
|||
EMAIL_CONNECTOR_USER_INPUT,
|
||||
EMAIL_CONNECTOR_PASSWORD_INPUT,
|
||||
EMAIL_CONNECTOR_SERVICE_SELECTOR,
|
||||
PREVIEW_HISTOGRAM,
|
||||
} from '../screens/create_new_rule';
|
||||
import { TOAST_ERROR } from '../screens/shared';
|
||||
import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline';
|
||||
|
@ -324,12 +324,12 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => {
|
|||
.find(QUERY_PREVIEW_BUTTON)
|
||||
.should('not.be.disabled')
|
||||
.click({ force: true });
|
||||
cy.get(EQL_QUERY_PREVIEW_HISTOGRAM)
|
||||
cy.get(PREVIEW_HISTOGRAM)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
if (text !== 'Hits') {
|
||||
cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true });
|
||||
cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits');
|
||||
cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Hits');
|
||||
}
|
||||
});
|
||||
cy.get(TOAST_ERROR).should('not.exist');
|
||||
|
|
|
@ -15,7 +15,7 @@ import { MatrixHistogramType } from '../../../../common/search_strategy/security
|
|||
import { UpdateDateRange } from '../charts/common';
|
||||
import { GlobalTimeArgs } from '../../containers/use_global_time';
|
||||
import { DocValueFields } from '../../../../common/search_strategy';
|
||||
import { Threshold } from '../../../detections/components/rules/query_preview';
|
||||
import { FieldValueThreshold } from '../../../detections/components/rules/threshold_input';
|
||||
|
||||
export type MatrixHistogramMappingTypes = Record<
|
||||
string,
|
||||
|
@ -77,7 +77,7 @@ export interface MatrixHistogramQueryProps {
|
|||
stackByField: string;
|
||||
startDate: string;
|
||||
histogramType: MatrixHistogramType;
|
||||
threshold?: Threshold;
|
||||
threshold?: FieldValueThreshold;
|
||||
skip?: boolean;
|
||||
isPtrIncluded?: boolean;
|
||||
includeMissingData?: boolean;
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
|
||||
import * as i18n from '../rule_preview/translations';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { PreviewCustomQueryHistogram } from './custom_histogram';
|
||||
|
||||
jest.mock('../../../../common/containers/use_global_time');
|
||||
|
||||
describe('PreviewCustomQueryHistogram', () => {
|
||||
const mockSetQuery = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useGlobalTime as jest.Mock).mockReturnValue({
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
isInitializing: false,
|
||||
to: '2020-07-08T08:20:18.966Z',
|
||||
setQuery: mockSetQuery,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it renders loader when isLoading is true', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewCustomQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
data={[]}
|
||||
totalCount={0}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
refetch={jest.fn()}
|
||||
isLoading
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle')
|
||||
).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING);
|
||||
});
|
||||
|
||||
test('it configures data and subtitle', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewCustomQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
data={[
|
||||
{ x: 1602247050000, y: 2314, g: 'All others' },
|
||||
{ x: 1602247162500, y: 3471, g: 'All others' },
|
||||
{ x: 1602247275000, y: 3369, g: 'All others' },
|
||||
]}
|
||||
totalCount={9154}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
refetch={jest.fn()}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle')
|
||||
).toEqual(i18n.QUERY_PREVIEW_TITLE(9154));
|
||||
expect(wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).props().data).toEqual(
|
||||
[
|
||||
{
|
||||
key: 'hits',
|
||||
value: [
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247050000,
|
||||
y: 2314,
|
||||
},
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247162500,
|
||||
y: 3471,
|
||||
},
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247275000,
|
||||
y: 3369,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes setQuery with id, inspect, isLoading and refetch', async () => {
|
||||
const mockRefetch = jest.fn();
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<PreviewCustomQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
data={[]}
|
||||
totalCount={0}
|
||||
inspect={{ dsl: ['some dsl'], response: ['query response'] }}
|
||||
refetch={mockRefetch}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockSetQuery).toHaveBeenCalledWith({
|
||||
id: 'queryPreviewCustomHistogramQuery',
|
||||
inspect: { dsl: ['some dsl'], response: ['query response'] },
|
||||
loading: false,
|
||||
refetch: mockRefetch,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo } from 'react';
|
||||
|
||||
import * as i18n from '../rule_preview/translations';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { getHistogramConfig } from '../rule_preview/helpers';
|
||||
import {
|
||||
ChartSeriesConfigs,
|
||||
ChartSeriesData,
|
||||
ChartData,
|
||||
} from '../../../../common/components/charts/common';
|
||||
import { InspectResponse } from '../../../../../public/types';
|
||||
import { inputsModel } from '../../../../common/store';
|
||||
import { PreviewHistogram } from './histogram';
|
||||
|
||||
export const ID = 'queryPreviewCustomHistogramQuery';
|
||||
|
||||
interface PreviewCustomQueryHistogramProps {
|
||||
to: string;
|
||||
from: string;
|
||||
isLoading: boolean;
|
||||
data: ChartData[];
|
||||
totalCount: number;
|
||||
inspect: InspectResponse;
|
||||
refetch: inputsModel.Refetch;
|
||||
}
|
||||
|
||||
export const PreviewCustomQueryHistogram = ({
|
||||
to,
|
||||
from,
|
||||
data,
|
||||
totalCount,
|
||||
inspect,
|
||||
refetch,
|
||||
isLoading,
|
||||
}: PreviewCustomQueryHistogramProps) => {
|
||||
const { setQuery, isInitializing } = useGlobalTime();
|
||||
|
||||
useEffect((): void => {
|
||||
if (!isLoading && !isInitializing) {
|
||||
setQuery({ id: ID, inspect, loading: isLoading, refetch });
|
||||
}
|
||||
}, [setQuery, inspect, isLoading, isInitializing, refetch]);
|
||||
|
||||
const barConfig = useMemo(
|
||||
(): ChartSeriesConfigs => getHistogramConfig(to, from, true),
|
||||
[from, to]
|
||||
);
|
||||
|
||||
const subtitle = useMemo(
|
||||
(): string =>
|
||||
isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||
[isLoading, totalCount]
|
||||
);
|
||||
|
||||
const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]);
|
||||
|
||||
return (
|
||||
<PreviewHistogram
|
||||
id={ID}
|
||||
data={chartData}
|
||||
barConfig={barConfig}
|
||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||
subtitle={subtitle}
|
||||
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER}
|
||||
isLoading={isLoading}
|
||||
dataTestSubj="queryPreviewCustomHistogram"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,152 +0,0 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
|
||||
import * as i18n from '../rule_preview/translations';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { PreviewEqlQueryHistogram } from './eql_histogram';
|
||||
|
||||
jest.mock('../../../../common/containers/use_global_time');
|
||||
|
||||
describe('PreviewEqlQueryHistogram', () => {
|
||||
const mockSetQuery = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useGlobalTime as jest.Mock).mockReturnValue({
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
isInitializing: false,
|
||||
to: '2020-07-08T08:20:18.966Z',
|
||||
setQuery: mockSetQuery,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it renders loader when isLoading is true', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewEqlQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
data={[]}
|
||||
totalCount={0}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
refetch={jest.fn()}
|
||||
isLoading
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle')
|
||||
).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING);
|
||||
});
|
||||
|
||||
test('it configures data and subtitle', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewEqlQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
data={[
|
||||
{ x: 1602247050000, y: 2314, g: 'All others' },
|
||||
{ x: 1602247162500, y: 3471, g: 'All others' },
|
||||
{ x: 1602247275000, y: 3369, g: 'All others' },
|
||||
]}
|
||||
totalCount={9154}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
refetch={jest.fn()}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle')
|
||||
).toEqual(i18n.QUERY_PREVIEW_TITLE(9154));
|
||||
expect(wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).props().data).toEqual([
|
||||
{
|
||||
key: 'hits',
|
||||
value: [
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247050000,
|
||||
y: 2314,
|
||||
},
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247162500,
|
||||
y: 3471,
|
||||
},
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247275000,
|
||||
y: 3369,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it invokes setQuery with id, inspect, isLoading and refetch', async () => {
|
||||
const mockRefetch = jest.fn();
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<PreviewEqlQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
data={[]}
|
||||
totalCount={0}
|
||||
inspect={{ dsl: ['some dsl'], response: ['query response'] }}
|
||||
refetch={mockRefetch}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockSetQuery).toHaveBeenCalledWith({
|
||||
id: 'queryEqlPreviewHistogramQuery',
|
||||
inspect: { dsl: ['some dsl'], response: ['query response'] },
|
||||
loading: false,
|
||||
refetch: mockRefetch,
|
||||
});
|
||||
});
|
||||
|
||||
test('it displays histogram', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewEqlQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
data={[
|
||||
{ x: 1602247050000, y: 2314, g: 'All others' },
|
||||
{ x: 1602247162500, y: 3471, g: 'All others' },
|
||||
{ x: 1602247275000, y: 3369, g: 'All others' },
|
||||
]}
|
||||
totalCount={9154}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
refetch={jest.fn()}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="sharedPreviewQueryNoHistogramAvailable"]').exists()
|
||||
).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo } from 'react';
|
||||
|
||||
import * as i18n from '../rule_preview/translations';
|
||||
import { getHistogramConfig } from '../rule_preview/helpers';
|
||||
import {
|
||||
ChartSeriesData,
|
||||
ChartSeriesConfigs,
|
||||
ChartData,
|
||||
} from '../../../../common/components/charts/common';
|
||||
import { InspectQuery } from '../../../../common/store/inputs/model';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { inputsModel } from '../../../../common/store';
|
||||
import { PreviewHistogram } from './histogram';
|
||||
|
||||
export const ID = 'queryEqlPreviewHistogramQuery';
|
||||
|
||||
interface PreviewEqlQueryHistogramProps {
|
||||
to: string;
|
||||
from: string;
|
||||
totalCount: number;
|
||||
isLoading: boolean;
|
||||
data: ChartData[];
|
||||
inspect: InspectQuery;
|
||||
refetch: inputsModel.Refetch;
|
||||
}
|
||||
|
||||
export const PreviewEqlQueryHistogram = ({
|
||||
from,
|
||||
to,
|
||||
totalCount,
|
||||
data,
|
||||
inspect,
|
||||
refetch,
|
||||
isLoading,
|
||||
}: PreviewEqlQueryHistogramProps) => {
|
||||
const { setQuery, isInitializing } = useGlobalTime();
|
||||
|
||||
useEffect((): void => {
|
||||
if (!isInitializing) {
|
||||
setQuery({ id: ID, inspect, loading: false, refetch });
|
||||
}
|
||||
}, [setQuery, inspect, isInitializing, refetch]);
|
||||
|
||||
const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]);
|
||||
|
||||
const subtitle = useMemo(
|
||||
(): string =>
|
||||
isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||
[isLoading, totalCount]
|
||||
);
|
||||
|
||||
const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]);
|
||||
|
||||
return (
|
||||
<PreviewHistogram
|
||||
id={ID}
|
||||
data={chartData}
|
||||
barConfig={barConfig}
|
||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||
subtitle={subtitle}
|
||||
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER_MAX_SIGNALS}
|
||||
isLoading={isLoading}
|
||||
dataTestSubj="queryPreviewEqlHistogram"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { PreviewHistogram } from './histogram';
|
||||
import { getHistogramConfig } from '../rule_preview/helpers';
|
||||
|
||||
describe('PreviewHistogram', () => {
|
||||
test('it renders loading icon if "isLoading" is true', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewHistogram
|
||||
id="previewHistogramId"
|
||||
data={[]}
|
||||
barConfig={{}}
|
||||
title="Hits"
|
||||
subtitle="500 hits"
|
||||
disclaimer="be ware"
|
||||
isLoading
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders chart if "isLoading" is true', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewHistogram
|
||||
id="previewHistogramId"
|
||||
data={[
|
||||
{
|
||||
key: 'hits',
|
||||
value: [
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247050000,
|
||||
y: 2314,
|
||||
},
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247162500,
|
||||
y: 3471,
|
||||
},
|
||||
{
|
||||
g: 'All others',
|
||||
x: 1602247275000,
|
||||
y: 3369,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
barConfig={getHistogramConfig('2020-07-08T08:20:18.966Z', '2020-07-07T08:20:18.966Z')}
|
||||
title="Hits"
|
||||
subtitle="500 hits"
|
||||
disclaimer="be ware"
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { BarChart } from '../../../../common/components/charts/barchart';
|
||||
import { Panel } from '../../../../common/components/panel';
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
import { ChartSeriesData, ChartSeriesConfigs } from '../../../../common/components/charts/common';
|
||||
|
||||
const LoadingChart = styled(EuiLoadingChart)`
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
interface PreviewHistogramProps {
|
||||
id: string;
|
||||
data: ChartSeriesData[];
|
||||
dataTestSubj?: string;
|
||||
barConfig: ChartSeriesConfigs;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
disclaimer: string;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const PreviewHistogram = ({
|
||||
id,
|
||||
data,
|
||||
dataTestSubj,
|
||||
barConfig,
|
||||
title,
|
||||
subtitle,
|
||||
disclaimer,
|
||||
isLoading,
|
||||
}: PreviewHistogramProps) => {
|
||||
return (
|
||||
<>
|
||||
<Panel height={300} data-test-subj={dataTestSubj}>
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
<EuiFlexItem grow={1}>
|
||||
<HeaderSection id={id} title={title} titleSize="xs" subtitle={subtitle} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
{isLoading ? (
|
||||
<LoadingChart size="l" data-test-subj="queryPreviewLoading" />
|
||||
) : (
|
||||
<BarChart
|
||||
configs={barConfig}
|
||||
barChart={data}
|
||||
stackByField={undefined}
|
||||
timelineId={undefined}
|
||||
data-test-subj="sharedPreviewQueryHistogram"
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>{disclaimer}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,502 +0,0 @@
|
|||
/*
|
||||
* 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 { of } from 'rxjs';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { PreviewQuery } from './';
|
||||
import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_response.mock';
|
||||
import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram';
|
||||
import { useEqlPreview } from '../../../../common/hooks/eql/';
|
||||
import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock';
|
||||
import type { FilterMeta } from '@kbn/es-query';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
euiSuperDatePickerWidth: '180px',
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/containers/matrix_histogram');
|
||||
jest.mock('../../../../common/hooks/eql/');
|
||||
|
||||
describe('PreviewQuery', () => {
|
||||
beforeEach(() => {
|
||||
useKibana().services.notifications.toasts.addError = jest.fn();
|
||||
|
||||
useKibana().services.notifications.toasts.addWarning = jest.fn();
|
||||
|
||||
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 1,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [],
|
||||
},
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
]);
|
||||
|
||||
(useEqlPreview as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 1,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it renders timeframe select and preview button on render', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'host.name:*', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewSelect"]').exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled
|
||||
).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders preview button disabled if "isDisabled" is true', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'host.name:*', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders preview button disabled if "query" is undefined', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={undefined}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders preview button enabled if query exists', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'host.name:"foo"', language: 'kql' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders preview button enabled if no query exists but filters do exist', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [{ meta: {} as FilterMeta, query: {} }],
|
||||
}}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders query histogram when rule type is query and preview button clicked', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'host.name:*', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
||||
|
||||
expect(mockCalls.length).toEqual(1);
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'host.name:*', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 2,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [],
|
||||
},
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
]);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders query histogram when rule type is saved_query and preview button clicked', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="saved_query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'host.name:*', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
||||
|
||||
expect(mockCalls.length).toEqual(1);
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders eql histogram when preview button clicked and rule type is eql', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="eql"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
||||
|
||||
expect(mockCalls.length).toEqual(1);
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="eql"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
(useEqlPreview as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 2,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [],
|
||||
},
|
||||
]);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="threshold"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={{
|
||||
field: ['agent.hostname'],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
}}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 500,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [{ key: 'siem-kibana', doc_count: 500 }],
|
||||
},
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
]);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
||||
|
||||
expect(mockCalls.length).toEqual(1);
|
||||
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={{
|
||||
field: ['agent.hostname'],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
}}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 500,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [
|
||||
{ key: 'siem-kibana', doc_count: 200 },
|
||||
{ key: 'siem-windows', doc_count: 300 },
|
||||
],
|
||||
},
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
]);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty array', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="threshold"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={{
|
||||
field: [],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
}}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
||||
|
||||
expect(mockCalls.length).toEqual(1);
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="threshold"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={{
|
||||
field: [' '],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
}}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
||||
|
||||
expect(mockCalls.length).toEqual(1);
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it hides histogram when timeframe changes', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="threshold"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy();
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="queryPreviewTimeframeSelect"] select')
|
||||
.at(0)
|
||||
.simulate('change', { target: { value: 'd' } });
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -1,362 +0,0 @@
|
|||
/*
|
||||
* 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, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
|
||||
import { Unit } from '@elastic/datemath';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSelect,
|
||||
EuiFormRow,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { debounce } from 'lodash/fp';
|
||||
|
||||
import { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import * as i18n from '../rule_preview/translations';
|
||||
import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram';
|
||||
import { MatrixHistogramType } from '../../../../../common/search_strategy';
|
||||
import { FieldValueQueryBar } from '../query_bar';
|
||||
import { PreviewEqlQueryHistogram } from './eql_histogram';
|
||||
import { useEqlPreview } from '../../../../common/hooks/eql/';
|
||||
import { PreviewThresholdQueryHistogram } from './threshold_histogram';
|
||||
import { formatDate } from '../../../../common/components/super_date_picker';
|
||||
import { State, queryPreviewReducer } from './reducer';
|
||||
import { isNoisy } from '../rule_preview/helpers';
|
||||
import { PreviewCustomQueryHistogram } from './custom_histogram';
|
||||
import { FieldValueThreshold } from '../threshold_input';
|
||||
|
||||
const Select = styled(EuiSelect)`
|
||||
width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth};
|
||||
`;
|
||||
|
||||
const PreviewButton = styled(EuiButton)`
|
||||
margin-left: 0;
|
||||
`;
|
||||
|
||||
export const initialState: State = {
|
||||
timeframeOptions: [],
|
||||
showHistogram: false,
|
||||
timeframe: 'h',
|
||||
warnings: [],
|
||||
queryFilter: undefined,
|
||||
toTime: '',
|
||||
fromTime: '',
|
||||
queryString: '',
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
thresholdFieldExists: false,
|
||||
showNonEqlHistogram: false,
|
||||
};
|
||||
|
||||
export type Threshold = FieldValueThreshold | undefined;
|
||||
|
||||
interface PreviewQueryProps {
|
||||
dataTestSubj: string;
|
||||
idAria: string;
|
||||
query: FieldValueQueryBar | undefined;
|
||||
index: string[];
|
||||
ruleType: Type;
|
||||
threshold: Threshold;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
export const PreviewQuery = ({
|
||||
ruleType,
|
||||
dataTestSubj,
|
||||
idAria,
|
||||
query,
|
||||
index,
|
||||
threshold,
|
||||
isDisabled,
|
||||
}: PreviewQueryProps) => {
|
||||
const [
|
||||
eqlQueryLoading,
|
||||
startEql,
|
||||
{
|
||||
totalCount: eqlQueryTotal,
|
||||
data: eqlQueryData,
|
||||
refetch: eqlQueryRefetch,
|
||||
inspect: eqlQueryInspect,
|
||||
},
|
||||
] = useEqlPreview();
|
||||
|
||||
const [
|
||||
{
|
||||
thresholdFieldExists,
|
||||
showNonEqlHistogram,
|
||||
timeframeOptions,
|
||||
showHistogram,
|
||||
timeframe,
|
||||
warnings,
|
||||
queryFilter,
|
||||
toTime,
|
||||
fromTime,
|
||||
queryString,
|
||||
},
|
||||
dispatch,
|
||||
] = useReducer(queryPreviewReducer(), {
|
||||
...initialState,
|
||||
toTime: formatDate('now-1h'),
|
||||
fromTime: formatDate('now'),
|
||||
});
|
||||
const [
|
||||
isMatrixHistogramLoading,
|
||||
{ inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets },
|
||||
startNonEql,
|
||||
] = useMatrixHistogram({
|
||||
errorMessage: i18n.QUERY_PREVIEW_ERROR,
|
||||
endDate: fromTime,
|
||||
startDate: toTime,
|
||||
filterQuery: queryFilter,
|
||||
indexNames: index,
|
||||
includeMissingData: false,
|
||||
histogramType: MatrixHistogramType.events,
|
||||
stackByField: 'event.category',
|
||||
threshold: ruleType === 'threshold' ? threshold : undefined,
|
||||
skip: true,
|
||||
});
|
||||
|
||||
const setQueryInfo = useCallback(
|
||||
(queryBar: FieldValueQueryBar | undefined, indices: string[], type: Type): void => {
|
||||
dispatch({
|
||||
type: 'setQueryInfo',
|
||||
queryBar,
|
||||
index: indices,
|
||||
ruleType: type,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const debouncedSetQueryInfo = useRef(debounce(500, setQueryInfo));
|
||||
|
||||
const setTimeframeSelect = useCallback(
|
||||
(selection: Unit): void => {
|
||||
dispatch({
|
||||
type: 'setTimeframeSelect',
|
||||
timeframe: selection,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setRuleTypeChange = useCallback(
|
||||
(type: Type): void => {
|
||||
dispatch({
|
||||
type: 'setResetRuleTypeChange',
|
||||
ruleType: type,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setWarnings = useCallback(
|
||||
(yikes: string[]): void => {
|
||||
dispatch({
|
||||
type: 'setWarnings',
|
||||
warnings: yikes,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setNoiseWarning = useCallback((): void => {
|
||||
dispatch({
|
||||
type: 'setNoiseWarning',
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
const setShowHistogram = useCallback(
|
||||
(show: boolean): void => {
|
||||
dispatch({
|
||||
type: 'setShowHistogram',
|
||||
show,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setThresholdValues = useCallback(
|
||||
(thresh: Threshold, type: Type): void => {
|
||||
dispatch({
|
||||
type: 'setThresholdQueryVals',
|
||||
threshold: thresh,
|
||||
ruleType: type,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedSetQueryInfo.current(query, index, ruleType);
|
||||
}, [index, query, ruleType]);
|
||||
|
||||
useEffect((): void => {
|
||||
setThresholdValues(threshold, ruleType);
|
||||
}, [setThresholdValues, threshold, ruleType]);
|
||||
|
||||
useEffect((): void => {
|
||||
setRuleTypeChange(ruleType);
|
||||
}, [ruleType, setRuleTypeChange]);
|
||||
|
||||
useEffect((): void => {
|
||||
switch (ruleType) {
|
||||
case 'eql':
|
||||
if (isNoisy(eqlQueryTotal, timeframe)) {
|
||||
setNoiseWarning();
|
||||
}
|
||||
break;
|
||||
case 'threshold':
|
||||
const totalHits = thresholdFieldExists ? buckets.length : matrixHistTotal;
|
||||
if (isNoisy(totalHits, timeframe)) {
|
||||
setNoiseWarning();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (isNoisy(matrixHistTotal, timeframe)) {
|
||||
setNoiseWarning();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
timeframe,
|
||||
matrixHistTotal,
|
||||
eqlQueryTotal,
|
||||
ruleType,
|
||||
setNoiseWarning,
|
||||
thresholdFieldExists,
|
||||
buckets.length,
|
||||
]);
|
||||
|
||||
const handlePreviewEqlQuery = useCallback(
|
||||
(to: string, from: string): void => {
|
||||
startEql({
|
||||
index,
|
||||
query: queryString,
|
||||
from,
|
||||
to,
|
||||
interval: timeframe,
|
||||
});
|
||||
},
|
||||
[startEql, index, queryString, timeframe]
|
||||
);
|
||||
|
||||
const handleSelectPreviewTimeframe = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
setTimeframeSelect(value as Unit);
|
||||
},
|
||||
[setTimeframeSelect]
|
||||
);
|
||||
|
||||
const handlePreviewClicked = useCallback((): void => {
|
||||
const to = formatDate('now');
|
||||
const from = formatDate(`now-1${timeframe}`);
|
||||
|
||||
setWarnings([]);
|
||||
setShowHistogram(true);
|
||||
|
||||
if (ruleType === 'eql') {
|
||||
handlePreviewEqlQuery(to, from);
|
||||
} else {
|
||||
startNonEql(to, from);
|
||||
}
|
||||
}, [setWarnings, setShowHistogram, ruleType, handlePreviewEqlQuery, startNonEql, timeframe]);
|
||||
|
||||
const previewButtonDisabled = useMemo(() => {
|
||||
return (
|
||||
isMatrixHistogramLoading ||
|
||||
eqlQueryLoading ||
|
||||
isDisabled ||
|
||||
query == null ||
|
||||
(query != null && query.query.query === '' && query.filters.length === 0)
|
||||
);
|
||||
}, [eqlQueryLoading, isDisabled, isMatrixHistogramLoading, query]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={i18n.QUERY_PREVIEW_LABEL}
|
||||
helpText={i18n.QUERY_PREVIEW_HELP_TEXT}
|
||||
error={undefined}
|
||||
isInvalid={false}
|
||||
data-test-subj={dataTestSubj}
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>
|
||||
<Select
|
||||
id="queryPreviewSelect"
|
||||
options={timeframeOptions}
|
||||
value={timeframe}
|
||||
onChange={handleSelectPreviewTimeframe}
|
||||
aria-label={i18n.QUERY_PREVIEW_SELECT_ARIA}
|
||||
disabled={isDisabled}
|
||||
data-test-subj="queryPreviewTimeframeSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PreviewButton
|
||||
fill
|
||||
isDisabled={previewButtonDisabled}
|
||||
onClick={handlePreviewClicked}
|
||||
data-test-subj="queryPreviewButton"
|
||||
>
|
||||
{i18n.QUERY_PREVIEW_BUTTON}
|
||||
</PreviewButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
{showNonEqlHistogram && showHistogram && (
|
||||
<PreviewCustomQueryHistogram
|
||||
to={toTime}
|
||||
from={fromTime}
|
||||
data={matrixHistoData}
|
||||
totalCount={matrixHistTotal}
|
||||
inspect={inspect}
|
||||
refetch={refetch}
|
||||
isLoading={isMatrixHistogramLoading}
|
||||
/>
|
||||
)}
|
||||
{ruleType === 'threshold' && thresholdFieldExists && showHistogram && (
|
||||
<PreviewThresholdQueryHistogram
|
||||
isLoading={isMatrixHistogramLoading}
|
||||
buckets={buckets}
|
||||
inspect={inspect}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
{ruleType === 'eql' && showHistogram && (
|
||||
<PreviewEqlQueryHistogram
|
||||
to={toTime}
|
||||
from={fromTime}
|
||||
totalCount={eqlQueryTotal}
|
||||
data={eqlQueryData}
|
||||
inspect={eqlQueryInspect}
|
||||
refetch={eqlQueryRefetch}
|
||||
isLoading={eqlQueryLoading}
|
||||
/>
|
||||
)}
|
||||
{showHistogram &&
|
||||
warnings.length > 0 &&
|
||||
warnings.map((warning, i) => (
|
||||
<Fragment key={`${warning}-${i}`}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut color="warning" iconType="help" data-test-subj="previewQueryWarning">
|
||||
<EuiText>
|
||||
<p>{warning}</p>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,502 +0,0 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
|
||||
import * as i18n from '../rule_preview/translations';
|
||||
import { Action, State, queryPreviewReducer } from './reducer';
|
||||
import { initialState } from './';
|
||||
|
||||
describe('queryPreviewReducer', () => {
|
||||
let reducer: (state: State, action: Action) => State;
|
||||
|
||||
beforeEach(() => {
|
||||
moment.tz.setDefault('UTC');
|
||||
reducer = queryPreviewReducer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
moment.tz.setDefault('Browser');
|
||||
});
|
||||
|
||||
describe('#setQueryInfo', () => {
|
||||
test('should not update state if queryBar undefined', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setQueryInfo',
|
||||
queryBar: undefined,
|
||||
index: ['foo-*'],
|
||||
ruleType: 'query',
|
||||
});
|
||||
|
||||
expect(update).toEqual(initialState);
|
||||
});
|
||||
|
||||
test('should reset showHistogram if queryBar undefined', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, showHistogram: true },
|
||||
{
|
||||
type: 'setQueryInfo',
|
||||
queryBar: undefined,
|
||||
index: ['foo-*'],
|
||||
ruleType: 'query',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should reset showHistogram if queryBar defined', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, showHistogram: true, warnings: ['uh oh'] },
|
||||
{
|
||||
type: 'setQueryInfo',
|
||||
queryBar: {
|
||||
query: { query: 'host.name:*', language: 'kuery' },
|
||||
filters: [{ meta: { alias: '', disabled: false, negate: false } }],
|
||||
},
|
||||
index: ['foo-*'],
|
||||
ruleType: 'query',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should pull the query, language, and filters from the action', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setQueryInfo',
|
||||
queryBar: {
|
||||
query: { query: 'host.name:*', language: 'kuery' },
|
||||
filters: [{ meta: { alias: '', disabled: false, negate: false } }],
|
||||
},
|
||||
index: ['foo-*'],
|
||||
ruleType: 'query',
|
||||
});
|
||||
|
||||
expect(update.language).toEqual('kuery');
|
||||
expect(update.queryString).toEqual('host.name:*');
|
||||
expect(update.filters).toEqual([
|
||||
{ meta: { alias: '', disabled: false, negate: false }, query: {} },
|
||||
]);
|
||||
});
|
||||
|
||||
test('should create the queryFilter if query type is not eql', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setQueryInfo',
|
||||
queryBar: {
|
||||
query: { query: 'host.name:*', language: 'kuery' },
|
||||
filters: [{ meta: { alias: '', disabled: false, negate: false } }],
|
||||
},
|
||||
index: ['foo-*'],
|
||||
ruleType: 'query',
|
||||
});
|
||||
|
||||
expect(update.queryFilter).toEqual({
|
||||
bool: {
|
||||
filter: [
|
||||
{ bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } },
|
||||
{},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should set query to empty string if it is not of type string', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setQueryInfo',
|
||||
queryBar: {
|
||||
query: { query: { not: 'a string' }, language: 'kuery' },
|
||||
filters: [{ meta: { alias: '', disabled: false, negate: false } }],
|
||||
},
|
||||
index: ['foo-*'],
|
||||
ruleType: 'query',
|
||||
});
|
||||
|
||||
expect(update.queryString).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setTimeframeSelect', () => {
|
||||
test('should update timeframe with that specified in action" ', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setTimeframeSelect',
|
||||
timeframe: 'd',
|
||||
});
|
||||
|
||||
expect(update.timeframe).toEqual('d');
|
||||
});
|
||||
|
||||
test('should reset warnings and showHistogram to false" ', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, showHistogram: true, warnings: ['blah'] },
|
||||
{
|
||||
type: 'setTimeframeSelect',
|
||||
timeframe: 'd',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
expect(update.warnings).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setResetRuleTypeChange', () => {
|
||||
test('should reset timeframe, warnings, and hide histogram on rule type change" ', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] },
|
||||
{
|
||||
type: 'setResetRuleTypeChange',
|
||||
ruleType: 'eql',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
expect(update.timeframe).toEqual('h');
|
||||
expect(update.warnings).toEqual([]);
|
||||
expect(update.showNonEqlHistogram).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should set timeframe options to hour and day if rule type is eql" ', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] },
|
||||
{
|
||||
type: 'setResetRuleTypeChange',
|
||||
ruleType: 'eql',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.timeframeOptions).toEqual([
|
||||
{
|
||||
text: 'Last hour',
|
||||
value: 'h',
|
||||
},
|
||||
{
|
||||
text: 'Last day',
|
||||
value: 'd',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is query" ', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] },
|
||||
{
|
||||
type: 'setResetRuleTypeChange',
|
||||
ruleType: 'query',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.showNonEqlHistogram).toBeTruthy();
|
||||
expect(update.timeframeOptions).toEqual([
|
||||
{
|
||||
text: 'Last hour',
|
||||
value: 'h',
|
||||
},
|
||||
{
|
||||
text: 'Last day',
|
||||
value: 'd',
|
||||
},
|
||||
{
|
||||
text: 'Last month',
|
||||
value: 'M',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is saved_query" ', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] },
|
||||
{
|
||||
type: 'setResetRuleTypeChange',
|
||||
ruleType: 'saved_query',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.showNonEqlHistogram).toBeTruthy();
|
||||
expect(update.timeframeOptions).toEqual([
|
||||
{
|
||||
text: 'Last hour',
|
||||
value: 'h',
|
||||
},
|
||||
{
|
||||
text: 'Last day',
|
||||
value: 'd',
|
||||
},
|
||||
{
|
||||
text: 'Last month',
|
||||
value: 'M',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is threshold and no threshold field is specified" ', () => {
|
||||
const update = reducer(
|
||||
{
|
||||
...initialState,
|
||||
timeframe: 'd',
|
||||
showHistogram: true,
|
||||
warnings: ['blah'],
|
||||
thresholdFieldExists: false,
|
||||
},
|
||||
{
|
||||
type: 'setResetRuleTypeChange',
|
||||
ruleType: 'threshold',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.showNonEqlHistogram).toBeTruthy();
|
||||
expect(update.timeframeOptions).toEqual([
|
||||
{
|
||||
text: 'Last hour',
|
||||
value: 'h',
|
||||
},
|
||||
{
|
||||
text: 'Last day',
|
||||
value: 'd',
|
||||
},
|
||||
{
|
||||
text: 'Last month',
|
||||
value: 'M',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should set "showNonEqlHist" to false and timeframe options to hour, day, and month if rule type is threshold and threshold field is specified" ', () => {
|
||||
const update = reducer(
|
||||
{
|
||||
...initialState,
|
||||
timeframe: 'd',
|
||||
showHistogram: true,
|
||||
warnings: ['blah'],
|
||||
thresholdFieldExists: true,
|
||||
},
|
||||
{
|
||||
type: 'setResetRuleTypeChange',
|
||||
ruleType: 'threshold',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.showNonEqlHistogram).toBeFalsy();
|
||||
expect(update.timeframeOptions).toEqual([
|
||||
{
|
||||
text: 'Last hour',
|
||||
value: 'h',
|
||||
},
|
||||
{
|
||||
text: 'Last day',
|
||||
value: 'd',
|
||||
},
|
||||
{
|
||||
text: 'Last month',
|
||||
value: 'M',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setWarnings', () => {
|
||||
test('should set warnings to that passed in action" ', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setWarnings',
|
||||
warnings: ['bad'],
|
||||
});
|
||||
|
||||
expect(update.warnings).toEqual(['bad']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setShowHistogram', () => {
|
||||
test('should set "setShowHistogram" to false if "action.show" is false', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setShowHistogram',
|
||||
show: false,
|
||||
});
|
||||
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should set "disableOr" to true if "action.show" is true', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setShowHistogram',
|
||||
show: true,
|
||||
});
|
||||
|
||||
expect(update.showHistogram).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setThresholdQueryVals', () => {
|
||||
test('should set thresholdFieldExists to true if threshold field is defined and not empty string', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setThresholdQueryVals',
|
||||
threshold: {
|
||||
field: ['agent.hostname'],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
ruleType: 'threshold',
|
||||
});
|
||||
|
||||
expect(update.thresholdFieldExists).toBeTruthy();
|
||||
expect(update.showNonEqlHistogram).toBeFalsy();
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
expect(update.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
test('should set thresholdFieldExists to false if threshold field is empty array', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setThresholdQueryVals',
|
||||
threshold: {
|
||||
field: [],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
ruleType: 'threshold',
|
||||
});
|
||||
|
||||
expect(update.thresholdFieldExists).toBeFalsy();
|
||||
expect(update.showNonEqlHistogram).toBeTruthy();
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
expect(update.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
test('should set thresholdFieldExists to false if threshold field is empty string', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setThresholdQueryVals',
|
||||
threshold: {
|
||||
field: [' '],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
ruleType: 'threshold',
|
||||
});
|
||||
|
||||
expect(update.thresholdFieldExists).toBeFalsy();
|
||||
expect(update.showNonEqlHistogram).toBeTruthy();
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
expect(update.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
test('should set showNonEqlHistogram to false if ruleType is eql', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setThresholdQueryVals',
|
||||
threshold: {
|
||||
field: ['agent.hostname'],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
ruleType: 'eql',
|
||||
});
|
||||
|
||||
expect(update.showNonEqlHistogram).toBeFalsy();
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
expect(update.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
test('should set showNonEqlHistogram to true if ruleType is query', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setThresholdQueryVals',
|
||||
threshold: {
|
||||
field: ['agent.hostname'],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
ruleType: 'query',
|
||||
});
|
||||
|
||||
expect(update.showNonEqlHistogram).toBeTruthy();
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
expect(update.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
test('should set showNonEqlHistogram to true if ruleType is saved_query', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setThresholdQueryVals',
|
||||
threshold: {
|
||||
field: ['agent.hostname'],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
ruleType: 'saved_query',
|
||||
});
|
||||
|
||||
expect(update.showNonEqlHistogram).toBeTruthy();
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
expect(update.warnings).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setToFrom', () => {
|
||||
test('should update to and from times to be an hour apart if timeframe is "h"', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, timeframe: 'h' },
|
||||
{
|
||||
type: 'setToFrom',
|
||||
}
|
||||
);
|
||||
|
||||
const dateFrom = moment(update.fromTime);
|
||||
const dateTo = moment(update.toTime);
|
||||
const diff = dateFrom.diff(dateTo);
|
||||
|
||||
// 3600000ms = 60 minutes
|
||||
// Sometimes test returns 3599999
|
||||
expect(Math.ceil(diff / 100000) * 100000).toEqual(3600000);
|
||||
});
|
||||
|
||||
test('should update to and from times to be a day apart if timeframe is "d"', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, timeframe: 'd' },
|
||||
{
|
||||
type: 'setToFrom',
|
||||
}
|
||||
);
|
||||
|
||||
const dateFrom = moment(update.fromTime);
|
||||
const dateTo = moment(update.toTime);
|
||||
const diff = dateFrom.diff(dateTo);
|
||||
|
||||
// 86400000 = 24 hours
|
||||
// Sometimes test returns 86399999
|
||||
expect(Math.ceil(diff / 100000) * 100000).toEqual(86400000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setNoiseWarning', () => {
|
||||
test('should add noise warning', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, warnings: ['uh oh'] },
|
||||
{
|
||||
type: 'setNoiseWarning',
|
||||
}
|
||||
);
|
||||
|
||||
expect(update.warnings).toEqual(['uh oh', i18n.QUERY_PREVIEW_NOISE_WARNING]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
* 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 { Unit } from '@elastic/datemath';
|
||||
import { EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
import { Type, Language } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import * as i18n from '../rule_preview/translations';
|
||||
import { ESQuery } from '../../../../../common/typed_json';
|
||||
import { FieldValueQueryBar } from '../query_bar';
|
||||
import { formatDate } from '../../../../common/components/super_date_picker';
|
||||
import { getInfoFromQueryBar, getTimeframeOptions } from '../rule_preview/helpers';
|
||||
import { Threshold } from '.';
|
||||
|
||||
export interface State {
|
||||
timeframeOptions: EuiSelectOption[];
|
||||
showHistogram: boolean;
|
||||
timeframe: Unit;
|
||||
warnings: string[];
|
||||
queryFilter: ESQuery | undefined;
|
||||
toTime: string;
|
||||
fromTime: string;
|
||||
queryString: string;
|
||||
language: Language;
|
||||
filters: Filter[];
|
||||
thresholdFieldExists: boolean;
|
||||
showNonEqlHistogram: boolean;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'setQueryInfo';
|
||||
queryBar: FieldValueQueryBar | undefined;
|
||||
index: string[];
|
||||
ruleType: Type;
|
||||
}
|
||||
| {
|
||||
type: 'setTimeframeSelect';
|
||||
timeframe: Unit;
|
||||
}
|
||||
| {
|
||||
type: 'setResetRuleTypeChange';
|
||||
ruleType: Type;
|
||||
}
|
||||
| {
|
||||
type: 'setWarnings';
|
||||
warnings: string[];
|
||||
}
|
||||
| {
|
||||
type: 'setShowHistogram';
|
||||
show: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'setThresholdQueryVals';
|
||||
threshold: Threshold;
|
||||
ruleType: Type;
|
||||
}
|
||||
| {
|
||||
type: 'setNoiseWarning';
|
||||
}
|
||||
| {
|
||||
type: 'setToFrom';
|
||||
};
|
||||
|
||||
export const queryPreviewReducer =
|
||||
() =>
|
||||
(state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'setQueryInfo': {
|
||||
if (action.queryBar != null) {
|
||||
const { queryString, language, filters, queryFilter } = getInfoFromQueryBar(
|
||||
action.queryBar,
|
||||
action.index,
|
||||
action.ruleType
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
queryString,
|
||||
language,
|
||||
filters,
|
||||
queryFilter,
|
||||
showHistogram: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
showHistogram: false,
|
||||
};
|
||||
}
|
||||
case 'setTimeframeSelect': {
|
||||
return {
|
||||
...state,
|
||||
timeframe: action.timeframe,
|
||||
showHistogram: false,
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
case 'setResetRuleTypeChange': {
|
||||
const showNonEqlHist =
|
||||
action.ruleType === 'query' ||
|
||||
action.ruleType === 'saved_query' ||
|
||||
(action.ruleType === 'threshold' && !state.thresholdFieldExists);
|
||||
|
||||
return {
|
||||
...state,
|
||||
showHistogram: false,
|
||||
timeframe: 'h',
|
||||
timeframeOptions: getTimeframeOptions(action.ruleType),
|
||||
showNonEqlHistogram: showNonEqlHist,
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
case 'setWarnings': {
|
||||
return {
|
||||
...state,
|
||||
warnings: action.warnings,
|
||||
};
|
||||
}
|
||||
case 'setShowHistogram': {
|
||||
return {
|
||||
...state,
|
||||
showHistogram: action.show,
|
||||
};
|
||||
}
|
||||
case 'setThresholdQueryVals': {
|
||||
const thresholdField =
|
||||
action.threshold != null &&
|
||||
action.threshold.field != null &&
|
||||
action.threshold.field.length > 0 &&
|
||||
action.threshold.field.every((field) => field.trim() !== '');
|
||||
const showNonEqlHist =
|
||||
action.ruleType === 'query' ||
|
||||
action.ruleType === 'saved_query' ||
|
||||
(action.ruleType === 'threshold' && !thresholdField);
|
||||
|
||||
return {
|
||||
...state,
|
||||
thresholdFieldExists: thresholdField,
|
||||
showNonEqlHistogram: showNonEqlHist,
|
||||
showHistogram: false,
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
case 'setToFrom': {
|
||||
return {
|
||||
...state,
|
||||
fromTime: formatDate('now'),
|
||||
toTime: formatDate(`now-1${state.timeframe}`),
|
||||
};
|
||||
}
|
||||
case 'setNoiseWarning': {
|
||||
return {
|
||||
...state,
|
||||
warnings: [...state.warnings, i18n.QUERY_PREVIEW_NOISE_WARNING],
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { PreviewThresholdQueryHistogram } from './threshold_histogram';
|
||||
|
||||
jest.mock('../../../../common/containers/use_global_time');
|
||||
|
||||
describe('PreviewThresholdQueryHistogram', () => {
|
||||
const mockSetQuery = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useGlobalTime as jest.Mock).mockReturnValue({
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
isInitializing: false,
|
||||
to: '2020-07-08T08:20:18.966Z',
|
||||
setQuery: mockSetQuery,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it renders loader when isLoading is true', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewThresholdQueryHistogram
|
||||
buckets={[]}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
refetch={jest.fn()}
|
||||
isLoading
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it configures buckets data', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PreviewThresholdQueryHistogram
|
||||
buckets={[
|
||||
{ key: 'siem_kibana', doc_count: 400 },
|
||||
{ key: 'bastion00.siem.estc.dev', doc_count: 80225 },
|
||||
{ key: 'es02.siem.estc.dev', doc_count: 1228 },
|
||||
]}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
refetch={jest.fn()}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[dataTestSubj="thresholdQueryPreviewHistogram"]').at(0).props().data
|
||||
).toEqual([
|
||||
{
|
||||
key: 'hits',
|
||||
value: [
|
||||
{ g: 'siem_kibana', x: 'siem_kibana', y: 400 },
|
||||
{ g: 'bastion00.siem.estc.dev', x: 'bastion00.siem.estc.dev', y: 80225 },
|
||||
{ g: 'es02.siem.estc.dev', x: 'es02.siem.estc.dev', y: 1228 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it invokes setQuery with id, inspect, isLoading and refetch', async () => {
|
||||
const mockRefetch = jest.fn();
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<PreviewThresholdQueryHistogram
|
||||
buckets={[
|
||||
{ key: 'siem_kibana', doc_count: 400 },
|
||||
{ key: 'bastion00.siem.estc.dev', doc_count: 80225 },
|
||||
{ key: 'es02.siem.estc.dev', doc_count: 1228 },
|
||||
]}
|
||||
inspect={{ dsl: ['some dsl'], response: ['query response'] }}
|
||||
refetch={mockRefetch}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockSetQuery).toHaveBeenCalledWith({
|
||||
id: 'queryPreviewThresholdHistogramQuery',
|
||||
inspect: { dsl: ['some dsl'], response: ['query response'] },
|
||||
loading: false,
|
||||
refetch: mockRefetch,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo } from 'react';
|
||||
|
||||
import * as i18n from '../rule_preview/translations';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { getThresholdHistogramConfig } from '../rule_preview/helpers';
|
||||
import { ChartSeriesConfigs, ChartSeriesData } from '../../../../common/components/charts/common';
|
||||
import { InspectResponse } from '../../../../../public/types';
|
||||
import { inputsModel } from '../../../../common/store';
|
||||
import { PreviewHistogram } from './histogram';
|
||||
|
||||
export const ID = 'queryPreviewThresholdHistogramQuery';
|
||||
|
||||
interface PreviewThresholdQueryHistogramProps {
|
||||
isLoading: boolean;
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
inspect: InspectResponse;
|
||||
refetch: inputsModel.Refetch;
|
||||
}
|
||||
|
||||
export const PreviewThresholdQueryHistogram = ({
|
||||
buckets,
|
||||
inspect,
|
||||
refetch,
|
||||
isLoading,
|
||||
}: PreviewThresholdQueryHistogramProps) => {
|
||||
const { setQuery, isInitializing } = useGlobalTime();
|
||||
|
||||
useEffect((): void => {
|
||||
if (!isLoading && !isInitializing) {
|
||||
setQuery({ id: ID, inspect, loading: isLoading, refetch });
|
||||
}
|
||||
}, [setQuery, inspect, isLoading, isInitializing, refetch]);
|
||||
|
||||
const { data, totalCount } = useMemo((): { data: ChartSeriesData[]; totalCount: number } => {
|
||||
const total = buckets.length;
|
||||
|
||||
const dataBuckets = buckets.map<{ x: string; y: number; g: string }>(
|
||||
({ key, doc_count: docCount }) => ({
|
||||
x: key,
|
||||
y: docCount,
|
||||
g: key,
|
||||
})
|
||||
);
|
||||
return {
|
||||
data: [{ key: 'hits', value: dataBuckets }],
|
||||
totalCount: total,
|
||||
};
|
||||
}, [buckets]);
|
||||
|
||||
const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(), []);
|
||||
|
||||
const subtitle = useMemo(
|
||||
(): string =>
|
||||
isLoading
|
||||
? i18n.QUERY_PREVIEW_SUBTITLE_LOADING
|
||||
: i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(totalCount),
|
||||
[isLoading, totalCount]
|
||||
);
|
||||
|
||||
return (
|
||||
<PreviewHistogram
|
||||
id={ID}
|
||||
data={data}
|
||||
barConfig={barConfig}
|
||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||
subtitle={subtitle}
|
||||
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER}
|
||||
isLoading={isLoading}
|
||||
dataTestSubj="thresholdQueryPreviewHistogram"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -14,31 +14,31 @@ import {
|
|||
|
||||
describe('query_preview/helpers', () => {
|
||||
describe('isNoisy', () => {
|
||||
test('returns true if timeframe selection is "Last hour" and average hits per hour is greater than one', () => {
|
||||
const isItNoisy = isNoisy(2, 'h');
|
||||
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');
|
||||
|
||||
expect(isItNoisy).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns false if timeframe selection is "Last hour" and average hits per hour is one', () => {
|
||||
const isItNoisy = isNoisy(1, 'h');
|
||||
test('returns false if timeframe selection is "Last hour" and average hits per hour is less than one execution duration', () => {
|
||||
const isItNoisy = isNoisy(10, 'h');
|
||||
|
||||
expect(isItNoisy).toBeFalsy();
|
||||
});
|
||||
|
||||
test('returns false if timeframe selection is "Last hour" and hits is 0', () => {
|
||||
const isItNoisy = isNoisy(1, 'h');
|
||||
const isItNoisy = isNoisy(0, 'h');
|
||||
|
||||
expect(isItNoisy).toBeFalsy();
|
||||
});
|
||||
|
||||
test('returns true if timeframe selection is "Last day" and average hits per hour is greater than one', () => {
|
||||
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');
|
||||
|
||||
expect(isItNoisy).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns false if timeframe selection is "Last day" and average hits per hour is one', () => {
|
||||
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');
|
||||
|
||||
expect(isItNoisy).toBeFalsy();
|
||||
|
@ -50,20 +50,20 @@ describe('query_preview/helpers', () => {
|
|||
expect(isItNoisy).toBeFalsy();
|
||||
});
|
||||
|
||||
test('returns true if timeframe selection is "Last month" and average hits per hour is greater than one', () => {
|
||||
const isItNoisy = isNoisy(1000, 'M');
|
||||
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');
|
||||
|
||||
expect(isItNoisy).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns false if timeframe selection is "Last month" and average hits per hour is one', () => {
|
||||
const isItNoisy = isNoisy(730, 'M');
|
||||
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');
|
||||
|
||||
expect(isItNoisy).toBeFalsy();
|
||||
});
|
||||
|
||||
test('returns false if timeframe selection is "Last month" and hits is 0', () => {
|
||||
const isItNoisy = isNoisy(1, 'M');
|
||||
const isItNoisy = isNoisy(0, 'M');
|
||||
|
||||
expect(isItNoisy).toBeFalsy();
|
||||
});
|
||||
|
@ -80,6 +80,7 @@ describe('query_preview/helpers', () => {
|
|||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
@ -94,6 +95,7 @@ describe('query_preview/helpers', () => {
|
|||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
@ -108,6 +110,7 @@ describe('query_preview/helpers', () => {
|
|||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
@ -122,6 +125,7 @@ describe('query_preview/helpers', () => {
|
|||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
@ -134,6 +138,20 @@ describe('query_preview/helpers', () => {
|
|||
index: ['test-*'],
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when there is no machine learning job id', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'threat_match',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
@ -148,6 +166,7 @@ describe('query_preview/helpers', () => {
|
|||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
});
|
||||
expect(isDisabled).toEqual(false);
|
||||
});
|
||||
|
@ -163,7 +182,7 @@ describe('query_preview/helpers', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('returns hour, day, and month options if ruleType is not eql', () => {
|
||||
test('returns hour, day, and month options if ruleType is not eql or threshold', () => {
|
||||
const options = getTimeframeOptions('query');
|
||||
|
||||
expect(options).toEqual([
|
||||
|
@ -172,6 +191,12 @@ describe('query_preview/helpers', () => {
|
|||
{ value: 'M', text: 'Last month' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns hour option if ruleType is threshold', () => {
|
||||
const options = getTimeframeOptions('threshold');
|
||||
|
||||
expect(options).toEqual([{ value: 'h', text: 'Last hour' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInfoFromQueryBar', () => {
|
||||
|
|
|
@ -24,13 +24,13 @@ import { ESQuery } from '../../../../../common/typed_json';
|
|||
*/
|
||||
export const isNoisy = (hits: number, timeframe: Unit): boolean => {
|
||||
if (timeframe === 'h') {
|
||||
return hits > 1;
|
||||
return hits > 20;
|
||||
} else if (timeframe === 'd') {
|
||||
return hits / 24 > 1;
|
||||
} else if (timeframe === 'w') {
|
||||
return hits / 168 > 1;
|
||||
} else if (timeframe === 'M') {
|
||||
return hits / 730 > 1;
|
||||
return hits / 30 > 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -54,6 +54,8 @@ export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
|
|||
{ value: 'd', text: i18n.LAST_DAY },
|
||||
{ value: 'w', text: i18n.LAST_WEEK },
|
||||
];
|
||||
} else if (ruleType === 'threshold') {
|
||||
return [{ value: 'h', text: i18n.LAST_HOUR }];
|
||||
} else {
|
||||
return [
|
||||
{ value: 'h', text: i18n.LAST_HOUR },
|
||||
|
@ -206,6 +208,7 @@ export const getIsRulePreviewDisabled = ({
|
|||
index,
|
||||
threatIndex,
|
||||
threatMapping,
|
||||
machineLearningJobId,
|
||||
}: {
|
||||
ruleType: Type;
|
||||
isQueryBarValid: boolean;
|
||||
|
@ -213,6 +216,7 @@ export const getIsRulePreviewDisabled = ({
|
|||
index: string[];
|
||||
threatIndex: string[];
|
||||
threatMapping: ThreatMapping;
|
||||
machineLearningJobId: string[];
|
||||
}) => {
|
||||
if (!isQueryBarValid || index.length === 0) return true;
|
||||
if (ruleType === 'threat_match') {
|
||||
|
@ -225,5 +229,8 @@ export const getIsRulePreviewDisabled = ({
|
|||
)
|
||||
return true;
|
||||
}
|
||||
if (ruleType === 'machine_learning') {
|
||||
return machineLearningJobId.length === 0;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { RulePreview, RulePreviewProps } from './';
|
||||
|
@ -45,6 +45,16 @@ const defaultProps: RulePreviewProps = {
|
|||
filters: [],
|
||||
query: { query: 'threat.indicator.file.hash.md5:*', language: 'kuery' },
|
||||
},
|
||||
threshold: {
|
||||
field: ['agent.hostname'],
|
||||
value: '200',
|
||||
cardinality: {
|
||||
field: ['user.name'],
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
anomalyThreshold: 50,
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
};
|
||||
|
||||
describe('PreviewQuery', () => {
|
||||
|
@ -76,43 +86,43 @@ describe('PreviewQuery', () => {
|
|||
});
|
||||
|
||||
test('it renders timeframe select and preview button on render', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<RulePreview {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="rule-preview"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="preview-time-frame"]').exists()).toBeTruthy();
|
||||
expect(wrapper.findByTestId('rule-preview')).toBeTruthy();
|
||||
expect(wrapper.findByTestId('preview-time-frame')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders preview button disabled if "isDisabled" is true', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<RulePreview {...defaultProps} isDisabled={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="preview-button"] button').props().disabled).toBeTruthy();
|
||||
expect(wrapper.getByTestId('queryPreviewButton').closest('button')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('it renders preview button enabled if "isDisabled" is false', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<RulePreview {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="preview-button"] button').props().disabled).toBeFalsy();
|
||||
expect(wrapper.getByTestId('queryPreviewButton').closest('button')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test('does not render histogram when there is no previewId', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<RulePreview {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="preview-histogram-panel"]').exists()).toBeFalsy();
|
||||
expect(wrapper.queryByTestId('[data-test-subj="preview-histogram-panel"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ import { getTimeframeOptions } from './helpers';
|
|||
import { CalloutGroup } from './callout_group';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { LoadingHistogram } from './loading_histogram';
|
||||
import { FieldValueThreshold } from '../threshold_input';
|
||||
|
||||
export interface RulePreviewProps {
|
||||
index: string[];
|
||||
|
@ -34,6 +35,9 @@ export interface RulePreviewProps {
|
|||
threatIndex: string[];
|
||||
threatMapping: ThreatMapping;
|
||||
threatQuery: FieldValueQueryBar;
|
||||
threshold: FieldValueThreshold;
|
||||
machineLearningJobId: string[];
|
||||
anomalyThreshold: number;
|
||||
}
|
||||
|
||||
const Select = styled(EuiSelect)`
|
||||
|
@ -44,6 +48,8 @@ const PreviewButton = styled(EuiButton)`
|
|||
margin-left: 0;
|
||||
`;
|
||||
|
||||
const defaultTimeRange: Unit = 'h';
|
||||
|
||||
const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
||||
index,
|
||||
isDisabled,
|
||||
|
@ -52,6 +58,9 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
}) => {
|
||||
const { spaces } = useKibana().services;
|
||||
const [spaceId, setSpaceId] = useState('');
|
||||
|
@ -61,7 +70,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
}
|
||||
}, [spaces]);
|
||||
|
||||
const [timeFrame, setTimeFrame] = useState<Unit>('h');
|
||||
const [timeFrame, setTimeFrame] = useState<Unit>(defaultTimeRange);
|
||||
const {
|
||||
addNoiseWarning,
|
||||
createPreview,
|
||||
|
@ -78,8 +87,16 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
timeFrame,
|
||||
ruleType,
|
||||
threatMapping,
|
||||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
});
|
||||
|
||||
// Resets the timeFrame to default when rule type is changed because not all time frames are supported by all rule types
|
||||
useEffect(() => {
|
||||
setTimeFrame(defaultTimeRange);
|
||||
}, [ruleType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
|
@ -108,7 +125,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
isLoading={isPreviewRequestInProgress}
|
||||
isDisabled={isDisabled}
|
||||
onClick={createPreview}
|
||||
data-test-subj="preview-button"
|
||||
data-test-subj="queryPreviewButton"
|
||||
>
|
||||
{i18n.QUERY_PREVIEW_BUTTON}
|
||||
</PreviewButton>
|
||||
|
@ -117,12 +134,16 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
{isPreviewRequestInProgress && <LoadingHistogram />}
|
||||
{!isPreviewRequestInProgress && previewId && spaceId && (
|
||||
{!isPreviewRequestInProgress && previewId && spaceId && query && (
|
||||
<PreviewHistogram
|
||||
ruleType={ruleType}
|
||||
timeFrame={timeFrame}
|
||||
previewId={previewId}
|
||||
addNoiseWarning={addNoiseWarning}
|
||||
spaceId={spaceId}
|
||||
threshold={threshold}
|
||||
query={query}
|
||||
index={index}
|
||||
/>
|
||||
)}
|
||||
<CalloutGroup items={errors} isError />
|
||||
|
|
|
@ -14,6 +14,7 @@ import { TestProviders } from '../../../../common/mock';
|
|||
import { usePreviewHistogram } from './use_preview_histogram';
|
||||
|
||||
import { PreviewHistogram } from './preview_histogram';
|
||||
import { mockQueryBar } from '../../../pages/detection_engine/rules/all/__mocks__/mock';
|
||||
|
||||
jest.mock('../../../../common/containers/use_global_time');
|
||||
jest.mock('./use_preview_histogram');
|
||||
|
@ -53,6 +54,9 @@ describe('PreviewHistogram', () => {
|
|||
timeFrame="M"
|
||||
previewId={'test-preview-id'}
|
||||
spaceId={'default'}
|
||||
ruleType={'query'}
|
||||
query={mockQueryBar}
|
||||
index={['']}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -86,6 +90,9 @@ describe('PreviewHistogram', () => {
|
|||
timeFrame="M"
|
||||
previewId={'test-preview-id'}
|
||||
spaceId={'default'}
|
||||
ruleType={'query'}
|
||||
query={mockQueryBar}
|
||||
index={['']}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -10,15 +10,18 @@ import usePrevious from 'react-use/lib/usePrevious';
|
|||
import { Unit } from '@elastic/datemath';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import * as i18n from './translations';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { getHistogramConfig, isNoisy } from './helpers';
|
||||
import { getHistogramConfig, getThresholdHistogramConfig, isNoisy } from './helpers';
|
||||
import { ChartSeriesConfigs, ChartSeriesData } from '../../../../common/components/charts/common';
|
||||
import { Panel } from '../../../../common/components/panel';
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
import { BarChart } from '../../../../common/components/charts/barchart';
|
||||
import { usePreviewHistogram } from './use_preview_histogram';
|
||||
import { formatDate } from '../../../../common/components/super_date_picker';
|
||||
import { FieldValueThreshold } from '../threshold_input';
|
||||
import { FieldValueQueryBar } from '../query_bar';
|
||||
|
||||
const LoadingChart = styled(EuiLoadingChart)`
|
||||
display: block;
|
||||
|
@ -32,6 +35,10 @@ interface PreviewHistogramProps {
|
|||
previewId: string;
|
||||
addNoiseWarning: () => void;
|
||||
spaceId: string;
|
||||
threshold?: FieldValueThreshold;
|
||||
ruleType: Type;
|
||||
query: FieldValueQueryBar;
|
||||
index: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_HISTOGRAM_HEIGHT = 300;
|
||||
|
@ -41,6 +48,10 @@ export const PreviewHistogram = ({
|
|||
previewId,
|
||||
addNoiseWarning,
|
||||
spaceId,
|
||||
threshold,
|
||||
ruleType,
|
||||
query,
|
||||
index,
|
||||
}: PreviewHistogramProps) => {
|
||||
const { setQuery, isInitializing } = useGlobalTime();
|
||||
|
||||
|
@ -48,12 +59,18 @@ export const PreviewHistogram = ({
|
|||
const to = useMemo(() => 'now', []);
|
||||
const startDate = useMemo(() => formatDate(from), [from]);
|
||||
const endDate = useMemo(() => formatDate(to), [to]);
|
||||
const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]);
|
||||
const isThresholdRule = useMemo(() => ruleType === 'threshold', [ruleType]);
|
||||
|
||||
const [isLoading, { data, inspect, totalCount, refetch }] = usePreviewHistogram({
|
||||
const [isLoading, { data, inspect, totalCount, refetch, buckets }] = usePreviewHistogram({
|
||||
previewId,
|
||||
startDate,
|
||||
endDate,
|
||||
spaceId,
|
||||
threshold: isThresholdRule ? threshold : undefined,
|
||||
query,
|
||||
index,
|
||||
ruleType,
|
||||
});
|
||||
|
||||
const previousPreviewId = usePrevious(previewId);
|
||||
|
@ -73,18 +90,42 @@ export const PreviewHistogram = ({
|
|||
}, [setQuery, inspect, isLoading, isInitializing, refetch, previewId]);
|
||||
|
||||
const barConfig = useMemo(
|
||||
(): ChartSeriesConfigs => getHistogramConfig(endDate, startDate, true),
|
||||
[endDate, startDate]
|
||||
(): ChartSeriesConfigs => getHistogramConfig(endDate, startDate, !isEqlRule),
|
||||
[endDate, startDate, isEqlRule]
|
||||
);
|
||||
|
||||
const thresholdBarConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(), []);
|
||||
|
||||
const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]);
|
||||
|
||||
const { thresholdChartData, thresholdTotalCount } = useMemo((): {
|
||||
thresholdChartData: ChartSeriesData[];
|
||||
thresholdTotalCount: number;
|
||||
} => {
|
||||
const total = buckets.length;
|
||||
const dataBuckets = buckets.map<{ x: string; y: number; g: string }>(
|
||||
({ key, doc_count: docCount }) => ({
|
||||
x: key,
|
||||
y: docCount,
|
||||
g: key,
|
||||
})
|
||||
);
|
||||
return {
|
||||
thresholdChartData: [{ key: 'hits', value: dataBuckets }],
|
||||
thresholdTotalCount: total,
|
||||
};
|
||||
}, [buckets]);
|
||||
|
||||
const subtitle = useMemo(
|
||||
(): string =>
|
||||
isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||
[isLoading, totalCount]
|
||||
isLoading
|
||||
? i18n.QUERY_PREVIEW_SUBTITLE_LOADING
|
||||
: isThresholdRule
|
||||
? i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(thresholdTotalCount)
|
||||
: i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||
[isLoading, totalCount, thresholdTotalCount, isThresholdRule]
|
||||
);
|
||||
|
||||
const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]);
|
||||
|
||||
return (
|
||||
<Panel height={DEFAULT_HISTOGRAM_HEIGHT} data-test-subj={'preview-histogram-panel'}>
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
|
@ -101,8 +142,8 @@ export const PreviewHistogram = ({
|
|||
<LoadingChart size="l" data-test-subj="preview-histogram-loading" />
|
||||
) : (
|
||||
<BarChart
|
||||
configs={barConfig}
|
||||
barChart={chartData}
|
||||
configs={isThresholdRule ? thresholdBarConfig : barConfig}
|
||||
barChart={isThresholdRule ? thresholdChartData : chartData}
|
||||
data-test-subj="preview-histogram-bar-chart"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram';
|
||||
import { MatrixHistogramType } from '../../../../../common/search_strategy';
|
||||
import { convertToBuildEsQuery } from '../../../../common/lib/keury';
|
||||
|
@ -12,12 +13,18 @@ import { getEsQueryConfig } from '../../../../../../../../src/plugins/data/commo
|
|||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { QUERY_PREVIEW_ERROR } from './translations';
|
||||
import { DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants';
|
||||
import { FieldValueThreshold } from '../threshold_input';
|
||||
import { FieldValueQueryBar } from '../query_bar';
|
||||
|
||||
interface PreviewHistogramParams {
|
||||
previewId: string | undefined;
|
||||
endDate: string;
|
||||
startDate: string;
|
||||
spaceId: string;
|
||||
threshold?: FieldValueThreshold;
|
||||
query: FieldValueQueryBar;
|
||||
index: string[];
|
||||
ruleType: Type;
|
||||
}
|
||||
|
||||
export const usePreviewHistogram = ({
|
||||
|
@ -25,24 +32,35 @@ export const usePreviewHistogram = ({
|
|||
startDate,
|
||||
endDate,
|
||||
spaceId,
|
||||
threshold,
|
||||
query,
|
||||
index,
|
||||
ruleType,
|
||||
}: PreviewHistogramParams) => {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const {
|
||||
query: { query: queryString, language },
|
||||
filters,
|
||||
} = query;
|
||||
|
||||
const [filterQuery] = convertToBuildEsQuery({
|
||||
const [filterQuery, error] = convertToBuildEsQuery({
|
||||
config: getEsQueryConfig(uiSettings),
|
||||
indexPattern: {
|
||||
fields: [
|
||||
{
|
||||
name: 'signal.rule.id',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
title: 'Preview',
|
||||
fields: [],
|
||||
title: index.join(),
|
||||
},
|
||||
queries: [{ query: `signal.rule.id:${previewId}`, language: 'kuery' }],
|
||||
filters: [],
|
||||
queries: [
|
||||
{ query: `signal.rule.id:${previewId}`, language: 'kuery' },
|
||||
{ query: queryString, language },
|
||||
],
|
||||
filters,
|
||||
});
|
||||
|
||||
const stackByField = useMemo(() => {
|
||||
const stackByDefault = ruleType === 'machine_learning' ? 'host.name' : 'event.category';
|
||||
return threshold?.field[0] ?? stackByDefault;
|
||||
}, [threshold, ruleType]);
|
||||
|
||||
const matrixHistogramRequest = useMemo(() => {
|
||||
return {
|
||||
endDate,
|
||||
|
@ -50,10 +68,13 @@ export const usePreviewHistogram = ({
|
|||
filterQuery,
|
||||
histogramType: MatrixHistogramType.preview,
|
||||
indexNames: [`${DEFAULT_PREVIEW_INDEX}-${spaceId}`],
|
||||
stackByField: 'event.category',
|
||||
stackByField,
|
||||
startDate,
|
||||
threshold,
|
||||
includeMissingData: false,
|
||||
skip: error != null,
|
||||
};
|
||||
}, [startDate, endDate, filterQuery, spaceId]);
|
||||
}, [startDate, endDate, filterQuery, spaceId, error, threshold, stackByField]);
|
||||
|
||||
return useMatrixHistogram(matrixHistogramRequest);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import { FieldValueQueryBar } from '../query_bar';
|
|||
import { QUERY_PREVIEW_NOISE_WARNING } from './translations';
|
||||
import { usePreviewRule } from '../../../containers/detection_engine/rules/use_preview_rule';
|
||||
import { formatPreviewRule } from '../../../pages/detection_engine/rules/create/helpers';
|
||||
import { FieldValueThreshold } from '../threshold_input';
|
||||
|
||||
interface PreviewRouteParams {
|
||||
isDisabled: boolean;
|
||||
|
@ -22,6 +23,9 @@ interface PreviewRouteParams {
|
|||
ruleType: Type;
|
||||
timeFrame: Unit;
|
||||
threatMapping: ThreatMapping;
|
||||
threshold: FieldValueThreshold;
|
||||
machineLearningJobId: string[];
|
||||
anomalyThreshold: number;
|
||||
}
|
||||
|
||||
export const usePreviewRoute = ({
|
||||
|
@ -33,10 +37,13 @@ export const usePreviewRoute = ({
|
|||
timeFrame,
|
||||
ruleType,
|
||||
threatMapping,
|
||||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
}: PreviewRouteParams) => {
|
||||
const [isRequestTriggered, setIsRequestTriggered] = useState(false);
|
||||
|
||||
const { isLoading, response, rule, setRule } = usePreviewRule();
|
||||
const { isLoading, response, rule, setRule } = usePreviewRule(timeFrame);
|
||||
const [warnings, setWarnings] = useState<string[]>(response.warnings ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -66,6 +73,9 @@ export const usePreviewRoute = ({
|
|||
timeFrame,
|
||||
ruleType,
|
||||
threatMapping,
|
||||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -79,6 +89,9 @@ export const usePreviewRoute = ({
|
|||
threatMapping,
|
||||
threatQuery,
|
||||
timeFrame,
|
||||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -93,6 +106,9 @@ export const usePreviewRoute = ({
|
|||
threatMapping,
|
||||
threatQuery,
|
||||
timeFrame,
|
||||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
@ -11,7 +11,6 @@ import { shallow } from 'enzyme';
|
|||
import { StepDefineRule } from './index';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../containers/detection_engine/alerts/use_preview_index');
|
||||
|
||||
describe('StepDefineRule', () => {
|
||||
it('renders correctly', () => {
|
||||
|
|
|
@ -55,10 +55,8 @@ import {
|
|||
import { EqlQueryBar } from '../eql_query_bar';
|
||||
import { ThreatMatchInput } from '../threatmatch_input';
|
||||
import { BrowserField, BrowserFields, useFetchIndex } from '../../../../common/containers/source';
|
||||
import { PreviewQuery } from '../query_preview';
|
||||
import { RulePreview } from '../rule_preview';
|
||||
import { getIsRulePreviewDisabled } from '../rule_preview/helpers';
|
||||
import { usePreviewIndex } from '../../../containers/detection_engine/alerts/use_preview_index';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -138,7 +136,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
onSubmit,
|
||||
setForm,
|
||||
}) => {
|
||||
usePreviewIndex();
|
||||
const mlCapabilities = useMlCapabilities();
|
||||
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
|
||||
const [indexModified, setIndexModified] = useState(false);
|
||||
|
@ -165,6 +162,8 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
threatQueryBar: formThreatQuery,
|
||||
threshold: formThreshold,
|
||||
threatMapping: formThreatMapping,
|
||||
machineLearningJobId: formMachineLearningJobId,
|
||||
anomalyThreshold: formAnomalyThreshold,
|
||||
},
|
||||
] = useFormData<DefineStepRule>({
|
||||
form,
|
||||
|
@ -179,6 +178,8 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
'threshold.cardinality.value',
|
||||
'threatIndex',
|
||||
'threatMapping',
|
||||
'machineLearningJobId',
|
||||
'anomalyThreshold',
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -186,12 +187,10 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
const [isThreatQueryBarValid, setIsThreatQueryBarValid] = useState(false);
|
||||
const index = formIndex || initialState.index;
|
||||
const threatIndex = formThreatIndex || initialState.threatIndex;
|
||||
const machineLearningJobId = formMachineLearningJobId ?? initialState.machineLearningJobId;
|
||||
const anomalyThreshold = formAnomalyThreshold ?? initialState.anomalyThreshold;
|
||||
const ruleType = formRuleType || initialState.ruleType;
|
||||
const isPreviewRouteEnabled = useMemo(() => ruleType === 'threat_match', [ruleType]);
|
||||
const isQueryPreviewEnabled = useMemo(
|
||||
() => ruleType !== 'machine_learning' && ruleType !== 'threat_match',
|
||||
[ruleType]
|
||||
);
|
||||
const isPreviewRouteEnabled = useMemo(() => ruleType !== 'threat_match', [ruleType]);
|
||||
const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(index);
|
||||
const aggregatableFields = Object.entries(browserFields).reduce<BrowserFields>(
|
||||
(groupAcc, [groupName, groupValue]) => {
|
||||
|
@ -505,20 +504,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
}}
|
||||
/>
|
||||
</Form>
|
||||
{isQueryPreviewEnabled && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<PreviewQuery
|
||||
dataTestSubj="ruleCreationQueryPreview"
|
||||
idAria="ruleCreationQueryPreview"
|
||||
ruleType={ruleType}
|
||||
index={index}
|
||||
query={formQuery}
|
||||
isDisabled={!isQueryBarValid || index.length === 0}
|
||||
threshold={formThreshold}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isPreviewRouteEnabled && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
|
@ -531,12 +516,16 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
index,
|
||||
threatIndex,
|
||||
threatMapping: formThreatMapping,
|
||||
machineLearningJobId,
|
||||
})}
|
||||
query={formQuery}
|
||||
ruleType={ruleType}
|
||||
threatIndex={threatIndex}
|
||||
threatQuery={formThreatQuery}
|
||||
threatMapping={formThreatMapping}
|
||||
threshold={formThreshold}
|
||||
machineLearningJobId={machineLearningJobId}
|
||||
anomalyThreshold={anomalyThreshold}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
getUserPrivilege,
|
||||
createSignalIndex,
|
||||
createHostIsolation,
|
||||
createPreviewIndex,
|
||||
} from './api';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
|
||||
|
@ -166,25 +165,6 @@ describe('Detections Alerts API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createPreviewIndex', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue({ acknowledged: true });
|
||||
});
|
||||
|
||||
test('check parameter url', async () => {
|
||||
await createPreviewIndex();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/preview/index', {
|
||||
method: 'POST',
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
const previewResp = await createPreviewIndex();
|
||||
expect(previewResp).toEqual({ acknowledged: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('createHostIsolation', () => {
|
||||
const postMock = coreStartMock.http.post;
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
DETECTION_ENGINE_INDEX_URL,
|
||||
DETECTION_ENGINE_PRIVILEGES_URL,
|
||||
ALERTS_AS_DATA_FIND_URL,
|
||||
DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL,
|
||||
DETECTION_ENGINE_RULES_PREVIEW,
|
||||
} from '../../../../../common/constants';
|
||||
import { HOST_METADATA_GET_ROUTE } from '../../../../../common/endpoint/constants';
|
||||
|
@ -134,15 +133,6 @@ export const createSignalIndex = async ({ signal }: BasicSignals): Promise<Alert
|
|||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create Preview Index if needed it
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const createPreviewIndex = async (): Promise<AlertsIndex> =>
|
||||
KibanaServices.get().http.fetch<AlertsIndex>(DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
/**
|
||||
* Create Preview if needed it
|
||||
* @throws An error if response is not OK
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Unit } from '@elastic/datemath';
|
||||
import { RULE_PREVIEW_INVOCATION_COUNT } from '../../../../../common/detection_engine/constants';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import {
|
||||
PreviewResponse,
|
||||
|
@ -23,11 +25,24 @@ const emptyPreviewRule: PreviewResponse = {
|
|||
warnings: [],
|
||||
};
|
||||
|
||||
export const usePreviewRule = () => {
|
||||
export const usePreviewRule = (timeframe: Unit = 'h') => {
|
||||
const [rule, setRule] = useState<CreateRulesSchema | null>(null);
|
||||
const [response, setResponse] = useState<PreviewResponse>(emptyPreviewRule);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { addError } = useAppToasts();
|
||||
let invocationCount = RULE_PREVIEW_INVOCATION_COUNT.HOUR;
|
||||
|
||||
switch (timeframe) {
|
||||
case 'd':
|
||||
invocationCount = RULE_PREVIEW_INVOCATION_COUNT.DAY;
|
||||
break;
|
||||
case 'w':
|
||||
invocationCount = RULE_PREVIEW_INVOCATION_COUNT.WEEK;
|
||||
break;
|
||||
case 'M':
|
||||
invocationCount = RULE_PREVIEW_INVOCATION_COUNT.MONTH;
|
||||
break;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!rule) {
|
||||
|
@ -45,7 +60,7 @@ export const usePreviewRule = () => {
|
|||
try {
|
||||
setIsLoading(true);
|
||||
const previewRuleResponse = await previewRule({
|
||||
rule: { ...transformOutput(rule), invocationCount: 1 },
|
||||
rule: { ...transformOutput(rule), invocationCount },
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
if (isSubscribed) {
|
||||
|
@ -67,7 +82,7 @@ export const usePreviewRule = () => {
|
|||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [rule, addError]);
|
||||
}, [rule, addError, invocationCount]);
|
||||
|
||||
return { isLoading, response, rule, setRule };
|
||||
};
|
||||
|
|
|
@ -44,6 +44,7 @@ import { CreateRulesSchema } from '../../../../../../common/detection_engine/sch
|
|||
import { stepDefineDefaultValue } from '../../../../components/rules/step_define_rule';
|
||||
import { stepAboutDefaultValue } from '../../../../components/rules/step_about_rule/default_value';
|
||||
import { stepActionsDefaultValue } from '../../../../components/rules/step_rule_actions';
|
||||
import { FieldValueThreshold } from '../../../../components/rules/threshold_input';
|
||||
|
||||
export const getTimeTypeValue = (time: string): { unit: string; value: number } => {
|
||||
const timeObj = {
|
||||
|
@ -266,7 +267,6 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep
|
|||
...(ruleType === 'query' &&
|
||||
ruleFields.queryBar?.saved_id && { type: 'saved_query' as Type }),
|
||||
};
|
||||
|
||||
return {
|
||||
...baseFields,
|
||||
...typeFields,
|
||||
|
@ -402,6 +402,9 @@ export const formatPreviewRule = ({
|
|||
ruleType,
|
||||
threatMapping,
|
||||
timeFrame,
|
||||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
}: {
|
||||
index: string[];
|
||||
threatIndex: string[];
|
||||
|
@ -410,6 +413,9 @@ export const formatPreviewRule = ({
|
|||
ruleType: Type;
|
||||
threatMapping: ThreatMapping;
|
||||
timeFrame: Unit;
|
||||
threshold: FieldValueThreshold;
|
||||
machineLearningJobId: string[];
|
||||
anomalyThreshold: number;
|
||||
}): CreateRulesSchema => {
|
||||
const defineStepData = {
|
||||
...stepDefineDefaultValue,
|
||||
|
@ -419,6 +425,9 @@ export const formatPreviewRule = ({
|
|||
threatIndex,
|
||||
threatQueryBar: threatQuery,
|
||||
threatMapping,
|
||||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
};
|
||||
const aboutStepData = {
|
||||
...stepAboutDefaultValue,
|
||||
|
@ -426,8 +435,8 @@ export const formatPreviewRule = ({
|
|||
description: 'Preview Rule',
|
||||
};
|
||||
const scheduleStepData = {
|
||||
from: `now-${timeFrame === 'w' ? '604830' : timeFrame === 'd' ? '86420' : '3610'}s`,
|
||||
interval: `30s`,
|
||||
from: `now-${timeFrame === 'M' ? '25h' : timeFrame === 'd' ? '65m' : '6m'}`,
|
||||
interval: `${timeFrame === 'M' ? '1d' : timeFrame === 'd' ? '1h' : '5m'}`,
|
||||
};
|
||||
return {
|
||||
...formatRule<CreateRulesSchema>(
|
||||
|
|
|
@ -27,7 +27,6 @@ jest.mock('react-router-dom', () => {
|
|||
});
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
jest.mock('../../../../containers/detection_engine/lists/use_lists_config');
|
||||
jest.mock('../../../../containers/detection_engine/alerts/use_preview_index');
|
||||
jest.mock('../../../../../common/components/link_to');
|
||||
jest.mock('../../../../components/user_info');
|
||||
jest.mock('../../../../../common/hooks/use_app_toasts');
|
||||
|
|
|
@ -85,7 +85,6 @@ jest.mock('react-router-dom', () => {
|
|||
});
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
jest.mock('../../../../containers/detection_engine/alerts/use_preview_index');
|
||||
|
||||
const mockRedirectLegacyUrl = jest.fn();
|
||||
const mockGetLegacyUrlConflict = jest.fn();
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
transformError,
|
||||
getIndexExists,
|
||||
getPolicyExists,
|
||||
setPolicy,
|
||||
createBootstrapIndex,
|
||||
} from '@kbn/securitysolution-es-utils';
|
||||
import type {
|
||||
AppClient,
|
||||
SecuritySolutionPluginRouter,
|
||||
SecuritySolutionRequestHandlerContext,
|
||||
} from '../../../../types';
|
||||
import { DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL } from '../../../../../common/constants';
|
||||
import { buildSiemResponse } from '../utils';
|
||||
import { getSignalsTemplate, SIGNALS_TEMPLATE_VERSION } from './get_signals_template';
|
||||
import previewPolicy from './preview_policy.json';
|
||||
import { getIndexVersion } from './get_index_version';
|
||||
import { isOutdated } from '../../migrations/helpers';
|
||||
import { templateNeedsUpdate } from './check_template_version';
|
||||
|
||||
export const createPreviewIndexRoute = (router: SecuritySolutionPluginRouter) => {
|
||||
router.post(
|
||||
{
|
||||
path: DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL,
|
||||
validate: false,
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
try {
|
||||
const siemClient = context.securitySolution?.getAppClient();
|
||||
if (!siemClient) {
|
||||
return siemResponse.error({ statusCode: 404 });
|
||||
}
|
||||
await createPreviewIndex(context, siemClient);
|
||||
|
||||
return response.ok({ body: { acknowledged: true } });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const createPreviewIndex = async (
|
||||
context: SecuritySolutionRequestHandlerContext,
|
||||
siemClient: AppClient
|
||||
) => {
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
const index = siemClient.getPreviewIndex();
|
||||
const spaceId = context.securitySolution.getSpaceId();
|
||||
|
||||
const indexExists = await getIndexExists(esClient, index);
|
||||
|
||||
const policyExists = await getPolicyExists(esClient, index);
|
||||
if (!policyExists) {
|
||||
await setPolicy(esClient, index, previewPolicy);
|
||||
}
|
||||
|
||||
const ruleDataService = context.securitySolution.getRuleDataService();
|
||||
const aadIndexAliasName = ruleDataService.getResourceName(`security.alerts-${spaceId}`);
|
||||
|
||||
if (await templateNeedsUpdate({ alias: index, esClient })) {
|
||||
await esClient.indices.putIndexTemplate({
|
||||
name: index,
|
||||
body: getSignalsTemplate(index, aadIndexAliasName) as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
if (indexExists) {
|
||||
const indexVersion = await getIndexVersion(esClient, index);
|
||||
if (isOutdated({ current: indexVersion, target: SIGNALS_TEMPLATE_VERSION })) {
|
||||
await esClient.indices.rollover({ alias: index });
|
||||
}
|
||||
} else {
|
||||
await createBootstrapIndex(esClient, index);
|
||||
}
|
||||
};
|
|
@ -7,10 +7,10 @@
|
|||
import moment from 'moment';
|
||||
import uuid from 'uuid';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { IRuleDataClient } from '../../../../../../rule_registry/server';
|
||||
import { buildSiemResponse } from '../utils';
|
||||
import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters';
|
||||
import { RuleParams } from '../../schemas/rule_schemas';
|
||||
import { signalRulesAlertType } from '../../signals/signal_rule_alert_type';
|
||||
import { createWarningsAndErrors } from '../../signals/preview/preview_rule_execution_log_client';
|
||||
import { parseInterval } from '../../signals/utils';
|
||||
import { buildMlAuthz } from '../../../machine_learning/authz';
|
||||
|
@ -33,22 +33,26 @@ import {
|
|||
import { ExecutorType } from '../../../../../../alerting/server/types';
|
||||
import { AlertInstance } from '../../../../../../alerting/server';
|
||||
import { ConfigType } from '../../../../config';
|
||||
import { IEventLogService } from '../../../../../../event_log/server';
|
||||
import { alertInstanceFactoryStub } from '../../signals/preview/alert_instance_factory_stub';
|
||||
import { CreateRuleOptions } from '../../rule_types/types';
|
||||
|
||||
enum InvocationCount {
|
||||
HOUR = 1,
|
||||
DAY = 24,
|
||||
WEEK = 168,
|
||||
}
|
||||
import { CreateRuleOptions, CreateSecurityRuleTypeWrapperProps } from '../../rule_types/types';
|
||||
import {
|
||||
createEqlAlertType,
|
||||
createIndicatorMatchAlertType,
|
||||
createMlAlertType,
|
||||
createQueryAlertType,
|
||||
createThresholdAlertType,
|
||||
} from '../../rule_types';
|
||||
import { createSecurityRuleTypeWrapper } from '../../rule_types/create_security_rule_type_wrapper';
|
||||
import { RULE_PREVIEW_INVOCATION_COUNT } from '../../../../../common/detection_engine/constants';
|
||||
|
||||
export const previewRulesRoute = async (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
config: ConfigType,
|
||||
ml: SetupPlugins['ml'],
|
||||
security: SetupPlugins['security'],
|
||||
ruleOptions: CreateRuleOptions
|
||||
ruleOptions: CreateRuleOptions,
|
||||
securityRuleTypeOptions: CreateSecurityRuleTypeWrapperProps,
|
||||
previewRuleDataClient: IRuleDataClient
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
|
@ -73,19 +77,22 @@ export const previewRulesRoute = async (
|
|||
return siemResponse.error({ statusCode: 404 });
|
||||
}
|
||||
|
||||
if (request.body.type !== 'threat_match') {
|
||||
return response.ok({ body: { errors: ['Not an indicator match rule'] } });
|
||||
}
|
||||
|
||||
let invocationCount = request.body.invocationCount;
|
||||
if (
|
||||
![InvocationCount.HOUR, InvocationCount.DAY, InvocationCount.WEEK].includes(
|
||||
invocationCount
|
||||
)
|
||||
![
|
||||
RULE_PREVIEW_INVOCATION_COUNT.HOUR,
|
||||
RULE_PREVIEW_INVOCATION_COUNT.DAY,
|
||||
RULE_PREVIEW_INVOCATION_COUNT.WEEK,
|
||||
RULE_PREVIEW_INVOCATION_COUNT.MONTH,
|
||||
].includes(invocationCount)
|
||||
) {
|
||||
return response.ok({ body: { errors: ['Invalid invocation count'] } });
|
||||
}
|
||||
|
||||
if (request.body.type === 'threat_match') {
|
||||
return response.ok({ body: { errors: ['Preview for rule type not supported'] } });
|
||||
}
|
||||
|
||||
const internalRule = convertCreateAPIToInternalSchema(request.body, siemClient, false);
|
||||
const previewRuleParams = internalRule.params;
|
||||
|
||||
|
@ -99,12 +106,17 @@ export const previewRulesRoute = async (
|
|||
await context.lists?.getExceptionListClient().createEndpointList();
|
||||
|
||||
const spaceId = siemClient.getSpaceId();
|
||||
const previewIndex = siemClient.getPreviewIndex();
|
||||
const previewId = uuid.v4();
|
||||
const username = security?.authc.getCurrentUser(request)?.username;
|
||||
const { previewRuleExecutionLogClient, warningsAndErrorsStore } = createWarningsAndErrors();
|
||||
const runState: Record<string, unknown> = {};
|
||||
|
||||
const previewRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
...securityRuleTypeOptions,
|
||||
ruleDataClient: previewRuleDataClient,
|
||||
ruleExecutionLogClientOverride: previewRuleExecutionLogClient,
|
||||
});
|
||||
|
||||
const runExecutors = async <
|
||||
TParams extends RuleParams,
|
||||
TState extends AlertTypeState,
|
||||
|
@ -134,7 +146,7 @@ export const previewRulesRoute = async (
|
|||
|
||||
const startedAt = moment();
|
||||
const parsedDuration = parseDuration(internalRule.schedule.interval) ?? 0;
|
||||
startedAt.subtract(moment.duration(parsedDuration * invocationCount));
|
||||
startedAt.subtract(moment.duration(parsedDuration * (invocationCount - 1)));
|
||||
|
||||
let previousStartedAt = null;
|
||||
|
||||
|
@ -175,27 +187,67 @@ export const previewRulesRoute = async (
|
|||
}
|
||||
};
|
||||
|
||||
const signalRuleAlertType = signalRulesAlertType({
|
||||
...ruleOptions,
|
||||
lists: context.lists,
|
||||
config,
|
||||
indexNameOverride: previewIndex,
|
||||
ruleExecutionLogClientOverride: previewRuleExecutionLogClient,
|
||||
// unused as we override the ruleExecutionLogClient
|
||||
eventLogService: {} as unknown as IEventLogService,
|
||||
eventsTelemetry: undefined,
|
||||
ml: undefined,
|
||||
refreshOverride: 'wait_for',
|
||||
});
|
||||
|
||||
await runExecutors(
|
||||
signalRuleAlertType.executor,
|
||||
signalRuleAlertType.id,
|
||||
signalRuleAlertType.name,
|
||||
previewRuleParams,
|
||||
() => true,
|
||||
alertInstanceFactoryStub
|
||||
);
|
||||
switch (previewRuleParams.type) {
|
||||
case 'query':
|
||||
const queryAlertType = previewRuleTypeWrapper(createQueryAlertType(ruleOptions));
|
||||
await runExecutors(
|
||||
queryAlertType.executor,
|
||||
queryAlertType.id,
|
||||
queryAlertType.name,
|
||||
previewRuleParams,
|
||||
() => true,
|
||||
alertInstanceFactoryStub
|
||||
);
|
||||
break;
|
||||
case 'threshold':
|
||||
const thresholdAlertType = previewRuleTypeWrapper(
|
||||
createThresholdAlertType(ruleOptions)
|
||||
);
|
||||
await runExecutors(
|
||||
thresholdAlertType.executor,
|
||||
thresholdAlertType.id,
|
||||
thresholdAlertType.name,
|
||||
previewRuleParams,
|
||||
() => true,
|
||||
alertInstanceFactoryStub
|
||||
);
|
||||
break;
|
||||
case 'threat_match':
|
||||
const threatMatchAlertType = previewRuleTypeWrapper(
|
||||
createIndicatorMatchAlertType(ruleOptions)
|
||||
);
|
||||
await runExecutors(
|
||||
threatMatchAlertType.executor,
|
||||
threatMatchAlertType.id,
|
||||
threatMatchAlertType.name,
|
||||
previewRuleParams,
|
||||
() => true,
|
||||
alertInstanceFactoryStub
|
||||
);
|
||||
break;
|
||||
case 'eql':
|
||||
const eqlAlertType = previewRuleTypeWrapper(createEqlAlertType(ruleOptions));
|
||||
await runExecutors(
|
||||
eqlAlertType.executor,
|
||||
eqlAlertType.id,
|
||||
eqlAlertType.name,
|
||||
previewRuleParams,
|
||||
() => true,
|
||||
alertInstanceFactoryStub
|
||||
);
|
||||
break;
|
||||
case 'machine_learning':
|
||||
const mlAlertType = previewRuleTypeWrapper(createMlAlertType(ruleOptions));
|
||||
await runExecutors(
|
||||
mlAlertType.executor,
|
||||
mlAlertType.id,
|
||||
mlAlertType.name,
|
||||
previewRuleParams,
|
||||
() => true,
|
||||
alertInstanceFactoryStub
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const errors = warningsAndErrorsStore
|
||||
.filter((item) => item.newStatus === RuleExecutionStatus.failed)
|
||||
|
@ -209,6 +261,14 @@ export const previewRulesRoute = async (
|
|||
)
|
||||
.map((item) => item.message);
|
||||
|
||||
// Refreshes alias to ensure index is able to be read before returning
|
||||
await context.core.elasticsearch.client.asInternalUser.indices.refresh(
|
||||
{
|
||||
index: previewRuleDataClient.indexNameWithNamespace(spaceId),
|
||||
},
|
||||
{ ignore: [404] }
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
previewId,
|
||||
|
|
|
@ -41,7 +41,7 @@ import aadFieldConversion from '../routes/index/signal_aad_mapping.json';
|
|||
|
||||
/* eslint-disable complexity */
|
||||
export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
|
||||
({ lists, logger, config, ruleDataClient, eventLogService }) =>
|
||||
({ lists, logger, config, ruleDataClient, eventLogService, ruleExecutionLogClientOverride }) =>
|
||||
(type) => {
|
||||
const { alertIgnoreFields: ignoreFields, alertMergeStrategy: mergeStrategy } = config;
|
||||
const persistenceRuleType = createPersistenceRuleTypeWrapper({ ruleDataClient, logger });
|
||||
|
@ -66,12 +66,14 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
|
|||
|
||||
const esClient = scopedClusterClient.asCurrentUser;
|
||||
|
||||
const ruleStatusClient = new RuleExecutionLogClient({
|
||||
underlyingClient: config.ruleExecutionLog.underlyingClient,
|
||||
savedObjectsClient,
|
||||
eventLogService,
|
||||
logger,
|
||||
});
|
||||
const ruleStatusClient = ruleExecutionLogClientOverride
|
||||
? ruleExecutionLogClientOverride
|
||||
: new RuleExecutionLogClient({
|
||||
underlyingClient: config.ruleExecutionLog.underlyingClient,
|
||||
savedObjectsClient,
|
||||
eventLogService,
|
||||
logger,
|
||||
});
|
||||
|
||||
const completeRule = {
|
||||
ruleConfig: rule,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
|||
import { EQL_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
|
||||
|
||||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
import { CompleteRule, eqlRuleParams, EqlRuleParams } from '../../schemas/rule_schemas';
|
||||
import { eqlRuleParams, EqlRuleParams } from '../../schemas/rule_schemas';
|
||||
import { eqlExecutor } from '../../signals/executors/eql';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
|
@ -67,7 +67,7 @@ export const createEqlAlertType = (
|
|||
exceptionItems,
|
||||
experimentalFeatures,
|
||||
logger,
|
||||
completeRule: completeRule as CompleteRule<EqlRuleParams>,
|
||||
completeRule,
|
||||
searchAfterSize,
|
||||
services,
|
||||
tuple,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
|||
import { INDICATOR_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
|
||||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
|
||||
import { CompleteRule, threatRuleParams, ThreatRuleParams } from '../../schemas/rule_schemas';
|
||||
import { threatRuleParams, ThreatRuleParams } from '../../schemas/rule_schemas';
|
||||
import { threatMatchExecutor } from '../../signals/executors/threat_match';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
|
@ -71,7 +71,7 @@ export const createIndicatorMatchAlertType = (
|
|||
eventsTelemetry: undefined,
|
||||
listClient,
|
||||
logger,
|
||||
completeRule: completeRule as CompleteRule<ThreatRuleParams>,
|
||||
completeRule,
|
||||
searchAfterSize,
|
||||
services,
|
||||
tuple,
|
||||
|
|
|
@ -9,11 +9,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
|||
import { ML_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
|
||||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
|
||||
import {
|
||||
CompleteRule,
|
||||
machineLearningRuleParams,
|
||||
MachineLearningRuleParams,
|
||||
} from '../../schemas/rule_schemas';
|
||||
import { machineLearningRuleParams, MachineLearningRuleParams } from '../../schemas/rule_schemas';
|
||||
import { mlExecutor } from '../../signals/executors/ml';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
|
@ -73,7 +69,7 @@ export const createMlAlertType = (
|
|||
listClient,
|
||||
logger,
|
||||
ml,
|
||||
completeRule: completeRule as CompleteRule<MachineLearningRuleParams>,
|
||||
completeRule,
|
||||
services,
|
||||
tuple,
|
||||
wrapHits,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
|||
import { QUERY_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
|
||||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
|
||||
import { CompleteRule, queryRuleParams, QueryRuleParams } from '../../schemas/rule_schemas';
|
||||
import { queryRuleParams, QueryRuleParams } from '../../schemas/rule_schemas';
|
||||
import { queryExecutor } from '../../signals/executors/query';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
|
@ -71,7 +71,7 @@ export const createQueryAlertType = (
|
|||
eventsTelemetry: undefined,
|
||||
listClient,
|
||||
logger,
|
||||
completeRule: completeRule as CompleteRule<QueryRuleParams>,
|
||||
completeRule,
|
||||
searchAfterSize,
|
||||
services,
|
||||
tuple,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
|||
import { THRESHOLD_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
|
||||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
|
||||
import { CompleteRule, thresholdRuleParams, ThresholdRuleParams } from '../../schemas/rule_schemas';
|
||||
import { thresholdRuleParams, ThresholdRuleParams } from '../../schemas/rule_schemas';
|
||||
import { thresholdExecutor } from '../../signals/executors/threshold';
|
||||
import { ThresholdAlertState } from '../../signals/types';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
@ -62,7 +62,7 @@ export const createThresholdAlertType = (
|
|||
exceptionItems,
|
||||
experimentalFeatures,
|
||||
logger,
|
||||
completeRule: completeRule as CompleteRule<ThresholdRuleParams>,
|
||||
completeRule,
|
||||
services,
|
||||
startedAt,
|
||||
state,
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
import { ExperimentalFeatures } from '../../../../common/experimental_features';
|
||||
import { IEventLogService } from '../../../../../event_log/server';
|
||||
import { AlertsFieldMap, RulesFieldMap } from '../../../../common/field_maps';
|
||||
import { IRuleExecutionLogClient } from '../rule_execution_log';
|
||||
|
||||
export interface SecurityAlertTypeReturnValue<TState extends AlertTypeState> {
|
||||
bulkCreateTimes: string[];
|
||||
|
@ -55,7 +56,7 @@ export interface RunOpts<TParams extends RuleParams> {
|
|||
bulkCreate: BulkCreate;
|
||||
exceptionItems: ExceptionListItemSchema[];
|
||||
listClient: ListClient;
|
||||
completeRule: CompleteRule<RuleParams>;
|
||||
completeRule: CompleteRule<TParams>;
|
||||
searchAfterSize: number;
|
||||
tuple: {
|
||||
to: Moment;
|
||||
|
@ -89,13 +90,18 @@ export type SecurityAlertType<
|
|||
) => Promise<SearchAfterAndBulkCreateReturnType & { state: TState }>;
|
||||
};
|
||||
|
||||
export type CreateSecurityRuleTypeWrapper = (options: {
|
||||
export interface CreateSecurityRuleTypeWrapperProps {
|
||||
lists: SetupPlugins['lists'];
|
||||
logger: Logger;
|
||||
config: ConfigType;
|
||||
ruleDataClient: IRuleDataClient;
|
||||
eventLogService: IEventLogService;
|
||||
}) => <
|
||||
ruleExecutionLogClientOverride?: IRuleExecutionLogClient;
|
||||
}
|
||||
|
||||
export type CreateSecurityRuleTypeWrapper = (
|
||||
options: CreateSecurityRuleTypeWrapperProps
|
||||
) => <
|
||||
TParams extends RuleParams,
|
||||
TState extends AlertTypeState,
|
||||
TInstanceContext extends AlertInstanceContext = {}
|
||||
|
|
|
@ -63,6 +63,7 @@ import { licenseService } from './lib/license';
|
|||
import { PolicyWatcher } from './endpoint/lib/policy/license_watch';
|
||||
import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet';
|
||||
import aadFieldConversion from './lib/detection_engine/routes/index/signal_aad_mapping.json';
|
||||
import previewPolicy from './lib/detection_engine/routes/index/preview_policy.json';
|
||||
import { registerEventLogProvider } from './lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider';
|
||||
import { getKibanaPrivilegesFeaturePrivileges, getCasesKibanaFeature } from './features';
|
||||
import { EndpointMetadataService } from './endpoint/services/metadata';
|
||||
|
@ -172,6 +173,7 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
|
||||
const { ruleDataService } = plugins.ruleRegistry;
|
||||
let ruleDataClient: IRuleDataClient | null = null;
|
||||
let previewRuleDataClient: IRuleDataClient | null = null;
|
||||
|
||||
// rule options are used both to create and preview rules.
|
||||
const ruleOptions: CreateRuleOptions = {
|
||||
|
@ -181,51 +183,58 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
version: pluginContext.env.packageInfo.version,
|
||||
};
|
||||
|
||||
if (isRuleRegistryEnabled) {
|
||||
const aliasesFieldMap: FieldMap = {};
|
||||
Object.entries(aadFieldConversion).forEach(([key, value]) => {
|
||||
aliasesFieldMap[key] = {
|
||||
type: 'alias',
|
||||
path: value,
|
||||
};
|
||||
});
|
||||
const aliasesFieldMap: FieldMap = {};
|
||||
Object.entries(aadFieldConversion).forEach(([key, value]) => {
|
||||
aliasesFieldMap[key] = {
|
||||
type: 'alias',
|
||||
path: value,
|
||||
};
|
||||
});
|
||||
|
||||
ruleDataClient = ruleDataService.initializeIndex({
|
||||
feature: SERVER_APP_ID,
|
||||
registrationContext: 'security',
|
||||
dataset: Dataset.alerts,
|
||||
componentTemplateRefs: [ECS_COMPONENT_TEMPLATE_NAME],
|
||||
componentTemplates: [
|
||||
{
|
||||
name: 'mappings',
|
||||
mappings: mappingFromFieldMap(
|
||||
{ ...technicalRuleFieldMap, ...alertsFieldMap, ...rulesFieldMap, ...aliasesFieldMap },
|
||||
false
|
||||
),
|
||||
},
|
||||
],
|
||||
secondaryAlias: config.signalsIndex,
|
||||
});
|
||||
const ruleDataServiceOptions = {
|
||||
feature: SERVER_APP_ID,
|
||||
registrationContext: 'security',
|
||||
dataset: Dataset.alerts,
|
||||
componentTemplateRefs: [ECS_COMPONENT_TEMPLATE_NAME],
|
||||
componentTemplates: [
|
||||
{
|
||||
name: 'mappings',
|
||||
mappings: mappingFromFieldMap(
|
||||
{ ...technicalRuleFieldMap, ...alertsFieldMap, ...rulesFieldMap, ...aliasesFieldMap },
|
||||
false
|
||||
),
|
||||
},
|
||||
],
|
||||
secondaryAlias: config.signalsIndex,
|
||||
};
|
||||
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
lists: plugins.lists,
|
||||
logger: this.logger,
|
||||
config: this.config,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
});
|
||||
ruleDataClient = ruleDataService.initializeIndex(ruleDataServiceOptions);
|
||||
const previewIlmPolicy = previewPolicy.policy;
|
||||
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createEqlAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createSavedQueryAlertType(ruleOptions))
|
||||
);
|
||||
plugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createIndicatorMatchAlertType(ruleOptions))
|
||||
);
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createMlAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createQueryAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createThresholdAlertType(ruleOptions)));
|
||||
}
|
||||
previewRuleDataClient = ruleDataService.initializeIndex({
|
||||
...ruleDataServiceOptions,
|
||||
additionalPrefix: '.preview',
|
||||
ilmPolicy: previewIlmPolicy,
|
||||
});
|
||||
|
||||
const securityRuleTypeOptions = {
|
||||
lists: plugins.lists,
|
||||
logger: this.logger,
|
||||
config: this.config,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
};
|
||||
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper(securityRuleTypeOptions);
|
||||
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createEqlAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createSavedQueryAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createIndicatorMatchAlertType(ruleOptions))
|
||||
);
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createMlAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createQueryAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createThresholdAlertType(ruleOptions)));
|
||||
|
||||
// TODO We need to get the endpoint routes inside of initRoutes
|
||||
initRoutes(
|
||||
|
@ -239,7 +248,9 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
logger,
|
||||
ruleDataClient,
|
||||
ruleOptions,
|
||||
core.getStartServices
|
||||
core.getStartServices,
|
||||
securityRuleTypeOptions,
|
||||
previewRuleDataClient
|
||||
);
|
||||
registerEndpointRoutes(router, endpointContext);
|
||||
registerLimitedConcurrencyRoutes(core);
|
||||
|
|
|
@ -61,11 +61,13 @@ import { ConfigType } from '../config';
|
|||
import { TelemetryEventsSender } from '../lib/telemetry/sender';
|
||||
import { installPrepackedTimelinesRoute } from '../lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines';
|
||||
import { previewRulesRoute } from '../lib/detection_engine/routes/rules/preview_rules_route';
|
||||
import { CreateRuleOptions } from '../lib/detection_engine/rule_types/types';
|
||||
import {
|
||||
CreateRuleOptions,
|
||||
CreateSecurityRuleTypeWrapperProps,
|
||||
} from '../lib/detection_engine/rule_types/types';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyCreateLegacyNotificationRoute } from '../lib/detection_engine/routes/rules/legacy_create_legacy_notification';
|
||||
import { createSourcererDataViewRoute } from '../lib/sourcerer/routes';
|
||||
import { createPreviewIndexRoute } from '../lib/detection_engine/routes/index/create_preview_index_route';
|
||||
|
||||
export const initRoutes = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
|
@ -78,7 +80,9 @@ export const initRoutes = (
|
|||
logger: Logger,
|
||||
ruleDataClient: IRuleDataClient | null,
|
||||
ruleOptions: CreateRuleOptions,
|
||||
getStartServices: StartServicesAccessor<StartPlugins>
|
||||
getStartServices: StartServicesAccessor<StartPlugins>,
|
||||
securityRuleTypeOptions: CreateSecurityRuleTypeWrapperProps,
|
||||
previewRuleDataClient: IRuleDataClient
|
||||
) => {
|
||||
const isRuleRegistryEnabled = ruleDataClient != null;
|
||||
// Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules
|
||||
|
@ -89,7 +93,15 @@ export const initRoutes = (
|
|||
patchRulesRoute(router, ml, isRuleRegistryEnabled);
|
||||
deleteRulesRoute(router, isRuleRegistryEnabled);
|
||||
findRulesRoute(router, logger, isRuleRegistryEnabled);
|
||||
previewRulesRoute(router, config, ml, security, ruleOptions);
|
||||
previewRulesRoute(
|
||||
router,
|
||||
config,
|
||||
ml,
|
||||
security,
|
||||
ruleOptions,
|
||||
securityRuleTypeOptions,
|
||||
previewRuleDataClient
|
||||
);
|
||||
|
||||
// Once we no longer have the legacy notifications system/"side car actions" this should be removed.
|
||||
legacyCreateLegacyNotificationRoute(router, logger);
|
||||
|
@ -140,9 +152,6 @@ export const initRoutes = (
|
|||
readIndexRoute(router, ruleDataService);
|
||||
deleteIndexRoute(router);
|
||||
|
||||
// Detection Engine Preview Index /api/detection_engine/preview/index
|
||||
createPreviewIndexRoute(router);
|
||||
|
||||
// Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags
|
||||
readTagsRoute(router, isRuleRegistryEnabled);
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./check_privileges'));
|
||||
loadTestFile(require.resolve('./create_index'));
|
||||
loadTestFile(require.resolve('./create_rules'));
|
||||
loadTestFile(require.resolve('./preview_rules'));
|
||||
loadTestFile(require.resolve('./create_rules_bulk'));
|
||||
loadTestFile(require.resolve('./create_ml'));
|
||||
loadTestFile(require.resolve('./create_threat_matching'));
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_PREVIEW } from '../../../../plugins/security_solution/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { deleteAllAlerts, getSimplePreviewRule, getSimpleRulePreviewOutput } from '../../utils';
|
||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const log = getService('log');
|
||||
|
||||
describe('create_rules', () => {
|
||||
describe('creating rules', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllAlerts(supertest, log);
|
||||
});
|
||||
|
||||
describe('elastic admin preview', () => {
|
||||
it('should create a single preview rule', async () => {
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_PREVIEW)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimplePreviewRule())
|
||||
.expect(200);
|
||||
expect(body).to.eql(getSimpleRulePreviewOutput(body.previewId));
|
||||
});
|
||||
|
||||
it("shouldn't cause a 409 conflict if we attempt to create the same rule_id twice", async () => {
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_PREVIEW)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimplePreviewRule())
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_PREVIEW)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimplePreviewRule())
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should throw an error if an invalid invocation count is used', async () => {
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_PREVIEW)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimplePreviewRule('', 3))
|
||||
.expect(200);
|
||||
const { errors } = getSimpleRulePreviewOutput(undefined, ['Invalid invocation count']);
|
||||
expect(body).to.eql({ errors });
|
||||
});
|
||||
});
|
||||
|
||||
describe('t1_analyst', () => {
|
||||
const role = ROLES.t1_analyst;
|
||||
|
||||
beforeEach(async () => {
|
||||
await createUserAndRole(getService, role);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteUserAndRole(getService, role);
|
||||
});
|
||||
|
||||
it('should NOT be able to preview a rule', async () => {
|
||||
await supertestWithoutAuth
|
||||
.post(DETECTION_ENGINE_RULES_PREVIEW)
|
||||
.auth(role, 'changeme')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimplePreviewRule())
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -33,6 +33,7 @@ import {
|
|||
QueryCreateSchema,
|
||||
EqlCreateSchema,
|
||||
ThresholdCreateSchema,
|
||||
PreviewRulesSchema,
|
||||
} from '../../plugins/security_solution/common/detection_engine/schemas/request';
|
||||
import { signalsMigrationType } from '../../plugins/security_solution/server/lib/detection_engine/migrations/saved_objects';
|
||||
import {
|
||||
|
@ -110,6 +111,27 @@ export const getSimpleRule = (ruleId = 'rule-1', enabled = false): QueryCreateSc
|
|||
query: 'user.name: root or user.name: admin',
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a typical simple preview rule for testing that is easy for most basic testing
|
||||
* @param ruleId
|
||||
* @param enabled The number of times the rule will be run through the executors. Defaulted to 20,
|
||||
* the execution time for the default interval time of 5m.
|
||||
*/
|
||||
export const getSimplePreviewRule = (
|
||||
ruleId = 'preview-rule-1',
|
||||
invocationCount = 20
|
||||
): PreviewRulesSchema => ({
|
||||
name: 'Simple Rule Query',
|
||||
description: 'Simple Rule Query',
|
||||
risk_score: 1,
|
||||
rule_id: ruleId,
|
||||
severity: 'high',
|
||||
index: ['auditbeat-*'],
|
||||
type: 'query',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
invocationCount,
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a typical signal testing rule that is easy for most basic testing of output of signals.
|
||||
* It starts out in an enabled true state. The from is set very far back to test the basics of signal
|
||||
|
@ -375,6 +397,24 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial
|
|||
version: 1,
|
||||
});
|
||||
|
||||
/**
|
||||
* This is the typical output of a simple rule preview, with errors and warnings coming up from the rule
|
||||
* execution process and a `previewId` generated server side for later preview querying
|
||||
*
|
||||
* @param previewId Rule id generated by the server itself
|
||||
* @param errors Errors returned by executor and route file, defaults to empty array
|
||||
* @param warnings Warnings returned by executor and route file, defaults to empty array
|
||||
*/
|
||||
export const getSimpleRulePreviewOutput = (
|
||||
previewId = undefined,
|
||||
errors: string[] = [],
|
||||
warnings: string[] = []
|
||||
) => ({
|
||||
errors,
|
||||
warnings,
|
||||
previewId,
|
||||
});
|
||||
|
||||
export const resolveSimpleRuleOutput = (
|
||||
ruleId = 'rule-1',
|
||||
enabled = false
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue