Skip to content

Commit e4fc48c

Browse files
authored
[Security Solution][Detections] - Rule creation query preview (#78985)
### Summary This PR introduces a preview histogram feature inside the rule creation workflow, that gives users insight to how effective the rule is for triggering alerts the user would like to see.
1 parent 041dfdd commit e4fc48c

File tree

30 files changed

+2055
-84
lines changed

30 files changed

+2055
-84
lines changed

x-pack/plugins/security_solution/common/detection_engine/types.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,48 @@ import { AlertAction } from '../../../alerts/common';
88
export type RuleAlertAction = Omit<AlertAction, 'actionTypeId'> & {
99
action_type_id: string;
1010
};
11+
12+
export type SearchTypes =
13+
| string
14+
| string[]
15+
| number
16+
| number[]
17+
| boolean
18+
| boolean[]
19+
| object
20+
| object[]
21+
| undefined;
22+
23+
export interface Explanation {
24+
value: number;
25+
description: string;
26+
details: Explanation[];
27+
}
28+
29+
export interface TotalValue {
30+
value: number;
31+
relation: string;
32+
}
33+
34+
export interface BaseHit<T> {
35+
_index: string;
36+
_id: string;
37+
_source: T;
38+
}
39+
40+
export interface EqlSequence<T> {
41+
join_keys: SearchTypes[];
42+
events: Array<BaseHit<T>>;
43+
}
44+
45+
export interface EqlSearchResponse<T> {
46+
is_partial: boolean;
47+
is_running: boolean;
48+
took: number;
49+
timed_out: boolean;
50+
hits: {
51+
total: TotalValue;
52+
sequences?: Array<EqlSequence<T>>;
53+
events?: Array<BaseHit<T>>;
54+
};
55+
}

x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface MatrixHistogramRequestOptions extends RequestBasicOptions {
3535
timerange: TimerangeInput;
3636
histogramType: MatrixHistogramType;
3737
stackByField: string;
38+
threshold?: { field: string | undefined; value: number } | undefined;
3839
inspect?: Maybe<Inspect>;
3940
}
4041

x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ const checkIfAnyValidSeriesExist = (
5151
export const BarChartBaseComponent = ({
5252
data,
5353
forceHiddenLegend = false,
54+
yAxisTitle,
5455
...chartConfigs
5556
}: {
5657
data: ChartSeriesData[];
5758
width: string | null | undefined;
5859
height: string | null | undefined;
60+
yAxisTitle?: string | undefined;
5961
configs?: ChartSeriesConfigs | undefined;
6062
forceHiddenLegend?: boolean;
6163
}) => {
@@ -115,6 +117,7 @@ export const BarChartBaseComponent = ({
115117
},
116118
}}
117119
tickFormat={yTickFormatter}
120+
title={yAxisTitle}
118121
/>
119122
</Chart>
120123
) : null;
@@ -158,6 +161,7 @@ export const BarChartComponent: React.FC<BarChartComponentProps> = ({
158161
[barChart, stackByField, timelineId]
159162
);
160163

164+
const yAxisTitle = get('yAxisTitle', configs);
161165
const customHeight = get('customHeight', configs);
162166
const customWidth = get('customWidth', configs);
163167
const chartHeight = getChartHeight(customHeight, height);
@@ -170,6 +174,7 @@ export const BarChartComponent: React.FC<BarChartComponentProps> = ({
170174
<BarChartBase
171175
configs={configs}
172176
data={barChart}
177+
yAxisTitle={yAxisTitle}
173178
forceHiddenLegend={stackByField != null}
174179
height={chartHeight}
175180
width={chartHeight}

x-pack/plugins/security_solution/public/common/components/charts/common.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,14 @@ export interface ChartSeriesConfigs {
4343
series?: {
4444
xScaleType?: ScaleType | undefined;
4545
yScaleType?: ScaleType | undefined;
46+
stackAccessors?: string[] | undefined;
4647
};
4748
axis?: {
4849
xTickFormatter?: TickFormatter | undefined;
4950
yTickFormatter?: TickFormatter | undefined;
51+
tickSize?: number | undefined;
5052
};
53+
yAxisTitle?: string | undefined;
5154
settings?: Partial<SettingsSpecProps>;
5255
}
5356

x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps &
3434
defaultStackByOption: MatrixHistogramOption;
3535
errorMessage: string;
3636
headerChildren?: React.ReactNode;
37+
footerChildren?: React.ReactNode;
3738
hideHistogramIfEmpty?: boolean;
3839
histogramType: MatrixHistogramType;
3940
id: string;
@@ -47,6 +48,7 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps &
4748
subtitle?: string | GetSubTitle;
4849
timelineId?: string;
4950
title: string | GetTitle;
51+
yTitle?: string | undefined;
5052
};
5153

5254
const DEFAULT_PANEL_HEIGHT = 300;
@@ -68,6 +70,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
6870
errorMessage,
6971
filterQuery,
7072
headerChildren,
73+
footerChildren,
7174
histogramType,
7275
hideHistogramIfEmpty = false,
7376
id,
@@ -86,6 +89,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
8689
title,
8790
titleSize,
8891
yTickFormatter,
92+
yTitle,
8993
}) => {
9094
const dispatch = useDispatch();
9195
const handleBrushEnd = useCallback(
@@ -114,8 +118,18 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
114118
onBrushEnd: handleBrushEnd,
115119
yTickFormatter,
116120
showLegend,
121+
yTitle,
117122
}),
118-
[chartHeight, startDate, legendPosition, endDate, handleBrushEnd, yTickFormatter, showLegend]
123+
[
124+
chartHeight,
125+
startDate,
126+
legendPosition,
127+
endDate,
128+
handleBrushEnd,
129+
yTickFormatter,
130+
showLegend,
131+
yTitle,
132+
]
119133
);
120134
const [isInitialLoading, setIsInitialLoading] = useState(true);
121135
const [selectedStackByOption, setSelectedStackByOption] = useState<MatrixHistogramOption>(
@@ -229,6 +243,11 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
229243
timelineId={timelineId}
230244
/>
231245
)}
246+
{footerChildren != null && (
247+
<EuiFlexGroup gutterSize="none" direction="row">
248+
{footerChildren}
249+
</EuiFlexGroup>
250+
)}
232251
</HistogramPanel>
233252
</InspectButtonContainer>
234253
{showSpacer && <EuiSpacer data-test-subj="spacer" size="l" />}

x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface MatrixHistogramQueryProps {
7070
stackByField: string;
7171
startDate: string;
7272
histogramType: MatrixHistogramType;
73+
threshold?: { field: string | undefined; value: number } | undefined;
7374
}
7475

7576
export interface MatrixHistogramProps extends MatrixHistogramBasicProps {
@@ -104,6 +105,7 @@ export interface BarchartConfigs {
104105
yTickFormatter: TickFormatter;
105106
tickSize: number;
106107
};
108+
yAxisTitle: string | undefined;
107109
settings: {
108110
legendPosition: Position;
109111
onBrushEnd: UpdateDateRange;

x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface GetBarchartConfigsProps {
1919
onBrushEnd: UpdateDateRange;
2020
yTickFormatter?: (value: number) => string;
2121
showLegend?: boolean;
22+
yTitle?: string | undefined;
2223
}
2324

2425
export const DEFAULT_CHART_HEIGHT = 174;
@@ -32,6 +33,7 @@ export const getBarchartConfigs = ({
3233
onBrushEnd,
3334
yTickFormatter,
3435
showLegend,
36+
yTitle,
3537
}: GetBarchartConfigsProps): BarchartConfigs => ({
3638
series: {
3739
xScaleType: ScaleType.Time,
@@ -43,6 +45,7 @@ export const getBarchartConfigs = ({
4345
yTickFormatter: yTickFormatter != null ? yTickFormatter : DEFAULT_Y_TICK_FORMATTER,
4446
tickSize: 8,
4547
},
48+
yAxisTitle: yTitle,
4649
settings: {
4750
legendPosition: legendPosition ?? Position.Right,
4851
onBrushEnd,

x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import deepEqual from 'fast-deep-equal';
8-
import { noop } from 'lodash/fp';
8+
import { getOr, noop } from 'lodash/fp';
99
import { useCallback, useEffect, useRef, useState } from 'react';
1010

1111
import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types';
@@ -32,6 +32,10 @@ export interface UseMatrixHistogramArgs {
3232
inspect: InspectResponse;
3333
refetch: inputsModel.Refetch;
3434
totalCount: number;
35+
buckets: Array<{
36+
key: string;
37+
doc_count: number;
38+
}>;
3539
}
3640

3741
const ID = 'matrixHistogramQuery';
@@ -44,6 +48,7 @@ export const useMatrixHistogram = ({
4448
indexNames,
4549
stackByField,
4650
startDate,
51+
threshold,
4752
}: MatrixHistogramQueryProps): [boolean, UseMatrixHistogramArgs] => {
4853
const { data, notifications } = useKibana().services;
4954
const refetch = useRef<inputsModel.Refetch>(noop);
@@ -63,6 +68,7 @@ export const useMatrixHistogram = ({
6368
to: endDate,
6469
},
6570
stackByField,
71+
threshold,
6672
});
6773

6874
const [matrixHistogramResponse, setMatrixHistogramResponse] = useState<UseMatrixHistogramArgs>({
@@ -73,6 +79,7 @@ export const useMatrixHistogram = ({
7379
},
7480
refetch: refetch.current,
7581
totalCount: -1,
82+
buckets: [],
7683
});
7784

7885
const hostsSearch = useCallback(
@@ -91,13 +98,18 @@ export const useMatrixHistogram = ({
9198
next: (response) => {
9299
if (isCompleteResponse(response)) {
93100
if (!didCancel) {
101+
const histogramBuckets: Array<{
102+
key: string;
103+
doc_count: number;
104+
}> = getOr([], 'rawResponse.aggregations.eventActionGroup.buckets', response);
94105
setLoading(false);
95106
setMatrixHistogramResponse((prevResponse) => ({
96107
...prevResponse,
97108
data: response.matrixHistogramData,
98109
inspect: getInspectResponse(response, prevResponse.inspect),
99110
refetch: refetch.current,
100111
totalCount: response.totalCount,
112+
buckets: histogramBuckets,
101113
}));
102114
}
103115
searchSubscription$.unsubscribe();
@@ -144,13 +156,14 @@ export const useMatrixHistogram = ({
144156
to: endDate,
145157
},
146158
stackByField,
159+
threshold,
147160
};
148161
if (!deepEqual(prevRequest, myRequest)) {
149162
return myRequest;
150163
}
151164
return prevRequest;
152165
});
153-
}, [indexNames, endDate, filterQuery, startDate, stackByField, histogramType]);
166+
}, [indexNames, endDate, filterQuery, startDate, stackByField, histogramType, threshold]);
154167

155168
useEffect(() => {
156169
hostsSearch(matrixHistogramRequest);

x-pack/plugins/security_solution/public/common/hooks/eql/api.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6+
import { Unit } from '@elastic/datemath';
67

78
import { HttpStart } from '../../../../../../../src/core/public';
89
import { DETECTION_ENGINE_EQL_VALIDATION_URL } from '../../../../common/constants';
910
import { EqlValidationSchema as EqlValidationRequest } from '../../../../common/detection_engine/schemas/request/eql_validation_schema';
1011
import { EqlValidationSchema as EqlValidationResponse } from '../../../../common/detection_engine/schemas/response/eql_validation_schema';
12+
import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public';
13+
import {
14+
EqlSearchStrategyRequest,
15+
EqlSearchStrategyResponse,
16+
} from '../../../../../data_enhanced/common';
17+
import { getEqlAggsData, getSequenceAggs } from './helpers';
18+
import { EqlPreviewResponse, Source } from './types';
19+
import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils';
20+
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
1121

1222
interface ApiParams {
1323
http: HttpStart;
@@ -29,3 +39,64 @@ export const validateEql = async ({
2939
signal,
3040
});
3141
};
42+
43+
interface AggsParams extends EqlValidationRequest {
44+
data: DataPublicPluginStart;
45+
interval: Unit;
46+
fromTime: string;
47+
toTime: string;
48+
signal: AbortSignal;
49+
}
50+
51+
export const getEqlPreview = async ({
52+
data,
53+
index,
54+
interval,
55+
query,
56+
fromTime,
57+
toTime,
58+
signal,
59+
}: AggsParams): Promise<EqlPreviewResponse> => {
60+
try {
61+
const response = await data.search
62+
.search<EqlSearchStrategyRequest, EqlSearchStrategyResponse<EqlSearchResponse<Source>>>(
63+
{
64+
params: {
65+
// @ts-expect-error allow_no_indices is missing on EqlSearch
66+
allow_no_indices: true,
67+
index: index.join(),
68+
body: {
69+
filter: {
70+
range: {
71+
'@timestamp': {
72+
gte: toTime,
73+
lte: fromTime,
74+
format: 'strict_date_optional_time',
75+
},
76+
},
77+
},
78+
query,
79+
// EQL requires a cap, otherwise it defaults to 10
80+
// It also sorts on ascending order, capping it at
81+
// something smaller like 20, made it so that some of
82+
// the more recent events weren't returned
83+
size: 100,
84+
},
85+
},
86+
},
87+
{
88+
strategy: 'eql',
89+
abortSignal: signal,
90+
}
91+
)
92+
.toPromise();
93+
94+
if (hasEqlSequenceQuery(query)) {
95+
return getSequenceAggs(response, interval, toTime, fromTime);
96+
} else {
97+
return getEqlAggsData(response, interval, toTime, fromTime);
98+
}
99+
} catch (err) {
100+
throw new Error(JSON.stringify(err));
101+
}
102+
};

0 commit comments

Comments
 (0)