Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Security Solution][Detections][Threshold Rules] Threshold multiple aggregations with cardinality #90826

Merged
merged 50 commits into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a4139ce
Remove unnecessary spreads
madirey Jan 14, 2021
b73b34a
Layout, round 1
madirey Jan 14, 2021
026a6d7
Merge branch 'master' of github.com:elastic/kibana into threshold-mul…
madirey Jan 25, 2021
8c5c889
Merge branch 'master' of github.com:elastic/kibana into threshold-mul…
madirey Jan 27, 2021
ed5f9b0
Revert "Layout, round 1"
madirey Jan 28, 2021
b2f31ba
Make threshold field an array
madirey Jan 28, 2021
0842e84
Add cardinality fields
madirey Jan 28, 2021
02c3b2c
Fix validation schema
madirey Feb 1, 2021
48f6545
Query for multi-aggs
madirey Feb 1, 2021
e435241
Merge branch 'master' of github.com:elastic/kibana into threshold-mul…
madirey Feb 1, 2021
70e4f50
Finish multi-agg aggregation
madirey Feb 1, 2021
cf7ef93
Translate to multi-agg buckets
madirey Feb 7, 2021
1ebfed0
Fix existing tests and add new test skeletons
madirey Feb 7, 2021
0786757
merge master, fix conflicts
madirey Feb 7, 2021
12a98bb
clean up
madirey Feb 9, 2021
6cba63a
Fix types
marshallmain Feb 11, 2021
78e77bc
Fix threshold_result data structure
madirey Feb 11, 2021
465c5a4
previous signals filter
madirey Feb 11, 2021
18d5363
Fix previous signal detection
madirey Feb 12, 2021
41a5ddb
Finish previous signal parsing
madirey Feb 12, 2021
19ed253
tying up loose ends
madirey Feb 14, 2021
733347c
merge master, fix conflicts
madirey Feb 15, 2021
c7eea31
Merge branch 'master' of github.com:elastic/kibana into threshold-mul…
madirey Feb 15, 2021
319e9db
Fix timeline view for multi-agg threshold signals
madirey Feb 15, 2021
e2a7d40
Fix build_bulk_body tests
madirey Feb 15, 2021
c6abdf5
test fixes
madirey Feb 16, 2021
741c75e
Add test for threshold bucket filters
madirey Feb 16, 2021
b277b04
Address comments
madirey Feb 16, 2021
8d1e922
Fixing schema errors
madirey Feb 16, 2021
6b8c8ed
Remove unnecessary comment
madirey Feb 16, 2021
a8dc733
Fix tests
madirey Feb 16, 2021
6fd0836
Fix types
madirey Feb 16, 2021
900ead0
Merge branch 'master' of github.com:elastic/kibana into threshold-mul…
madirey Feb 16, 2021
37956ca
linting
madirey Feb 17, 2021
7956953
linting
madirey Feb 17, 2021
af5ed84
Fixes
madirey Feb 17, 2021
ee103c1
Handle pre-7.12 threshold format in timeline view
madirey Feb 17, 2021
2094d58
missing null check
madirey Feb 17, 2021
bed1faf
adding in follow-up pr
madirey Feb 17, 2021
e979d12
Handle pre-7.12 filters
madirey Feb 17, 2021
9dab01e
Merge branch 'master' of github.com:elastic/kibana into threshold-mul…
madirey Feb 17, 2021
3edc7f2
unnecessary change
madirey Feb 17, 2021
13821bf
Revert "unnecessary change"
madirey Feb 17, 2021
f88cf66
linting
madirey Feb 17, 2021
3e09b24
Fix rule schemas
madirey Feb 17, 2021
6eafa8d
Merge branch 'master' of github.com:elastic/kibana into threshold-mul…
madirey Feb 17, 2021
db6dfa5
Fix tests
madirey Feb 17, 2021
9ba09b6
Merge branch 'master' of github.com:elastic/kibana into threshold-mul…
madirey Feb 18, 2021
b6fd98b
merge master, fix conflicts
madirey Feb 18, 2021
5c503fc
more fixing conflicts
madirey Feb 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x-pack/plugins/osquery/common/ecs/rule/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface RuleEcs {
tags?: string[];
threat?: unknown;
threshold?: {
field: string;
field: string | string[];
value: number;
};
type?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,12 +459,21 @@ export type Threats = t.TypeOf<typeof threats>;
export const threatsOrUndefined = t.union([threats, t.undefined]);
export type ThreatsOrUndefined = t.TypeOf<typeof threatsOrUndefined>;

export const threshold = t.exact(
t.type({
field: t.string,
value: PositiveIntegerGreaterThanZero,
})
);
export const threshold = t.intersection([
t.exact(
t.type({
field: t.union([t.string, t.array(t.string)]),
value: PositiveIntegerGreaterThanZero,
})
),
t.exact(
t.partial({
cardinality_field: t.union([t.string, t.array(t.string), t.undefined]),
cardinality_value: t.union([PositiveInteger, t.undefined]), // TODO: cardinality_value should be set if cardinality_field is set
madirey marked this conversation as resolved.
Show resolved Hide resolved
})
),
]);
// TODO: codec to transform threshold field string to string[] ?
export type Threshold = t.TypeOf<typeof threshold>;

export const thresholdOrUndefined = t.union([threshold, t.undefined]);
Expand Down
5 changes: 1 addition & 4 deletions x-pack/plugins/security_solution/common/ecs/rule/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ export interface RuleEcs {
severity?: string[];
tags?: string[];
threat?: unknown;
threshold?: {
field: string;
value: number;
};
threshold?: unknown;
type?: string[];
size?: string[];
to?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export interface SignalEcs {
group?: {
id?: string[];
};
threshold_result?: unknown;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ export interface MatrixHistogramRequestOptions extends RequestBasicOptions {
timerange: TimerangeInput;
histogramType: MatrixHistogramType;
stackByField: string;
threshold?: { field: string | undefined; value: number } | undefined;
threshold?:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like a few changes similar to this one were made in this PR, would it be worth pulling this out into its own type shared between the updated code? not a blocker, but might be a nice follow up.

| {
field: string | string[] | undefined;
value: number;
cardinality_field?: string;
cardinality_value?: number;
}
| undefined;
inspect?: Maybe<Inspect>;
isPtrIncluded?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ export interface MatrixHistogramQueryProps {
stackByField: string;
startDate: string;
histogramType: MatrixHistogramType;
threshold?: { field: string | undefined; value: number } | undefined;
threshold?:
| {
field: string | string[] | undefined;
value: number;
cardinality_field?: string | undefined;
cardinality_value?: number;
}
| undefined;
skip?: boolean;
isPtrIncluded?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/* eslint-disable complexity */

import dateMath from '@elastic/datemath';
import { get, getOr, isEmpty, find } from 'lodash/fp';
import { getOr, isEmpty } from 'lodash/fp';
import moment from 'moment';
import { i18n } from '@kbn/i18n';

Expand Down Expand Up @@ -38,15 +38,18 @@ import {
replaceTemplateFieldFromDataProviders,
} from './helpers';
import { KueryFilterQueryKind } from '../../../common/store';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
import {
DataProvider,
QueryOperator,
} from '../../../timelines/components/timeline/data_providers/data_provider';

export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
return {
query: {
bool: {
filter: {
terms: {
_id: [...eventIds],
_id: eventIds,
},
},
},
Expand Down Expand Up @@ -131,22 +134,48 @@ export const getThresholdAggregationDataProvider = (
ecsData: Ecs,
nonEcsData: TimelineNonEcsData[]
): DataProvider[] => {
const aggregationField = ecsData.signal?.rule?.threshold?.field!;
const aggregationValue =
get(aggregationField, ecsData) ?? find(['field', aggregationField], nonEcsData)?.value;
const dataProviderValue = Array.isArray(aggregationValue)
? aggregationValue[0]
: aggregationValue;
const threshold = ecsData.signal?.rule?.threshold as string[];

let aggField: string[] = [];
let thresholdResult: {
terms?: Array<{
field?: string;
value: string;
}>;
count: number;
};

if (!dataProviderValue) {
return [];
try {
thresholdResult = JSON.parse((ecsData.signal?.threshold_result as string[])[0]);
aggField = JSON.parse(threshold[0]).field;
} catch (err) {
thresholdResult = {
terms: [
{
field: (ecsData.rule?.threshold as { field: string }).field,
value: (ecsData.signal?.threshold_result as { value: string }).value,
},
],
count: (ecsData.signal?.threshold_result as { count: number }).count,
};
}

const aggregationFieldId = aggregationField.replace('.', '-');
const aggregationFields = Array.isArray(aggField) ? aggField : [aggField];

return aggregationFields.reduce<DataProvider[]>((acc, aggregationField, i) => {
const aggregationValue = (thresholdResult.terms ?? []).filter(
(term: { field?: string | undefined; value: string }) => term.field === aggregationField
)[0].value;
const dataProviderValue = Array.isArray(aggregationValue)
? aggregationValue[0]
: aggregationValue;

return [
{
and: [],
if (!dataProviderValue) {
return acc;
}

const aggregationFieldId = aggregationField.replace('.', '-');
const dataProviderPartial = {
id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`,
name: aggregationField,
enabled: true,
Expand All @@ -155,10 +184,23 @@ export const getThresholdAggregationDataProvider = (
queryMatch: {
field: aggregationField,
value: dataProviderValue,
operator: ':',
operator: ':' as QueryOperator,
},
},
];
};

if (i === 0) {
return [
...acc,
{
...dataProviderPartial,
and: [],
},
];
} else {
acc[0].and.push(dataProviderPartial);
return acc;
}
}, []);
};

export const isEqlRuleWithGroupId = (ecsData: Ecs) =>
Expand Down Expand Up @@ -275,7 +317,7 @@ export const sendAlertToTimelineAction = async ({
...timelineDefaults,
description: `_id: ${ecsData._id}`,
filters: getFiltersFromRule(ecsData.signal?.rule?.filters as string[]),
dataProviders: [...getThresholdAggregationDataProvider(ecsData, nonEcsData)],
dataProviders: getThresholdAggregationDataProvider(ecsData, nonEcsData),
id: TimelineId.active,
indexNames: [],
dateRange: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,12 @@ describe('PreviewQuery', () => {
idAria="queryPreview"
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
index={['foo-*']}
threshold={{ field: 'agent.hostname', value: 200 }}
threshold={{
field: 'agent.hostname',
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
}}
isDisabled={false}
/>
</TestProviders>
Expand Down Expand Up @@ -330,7 +335,12 @@ describe('PreviewQuery', () => {
idAria="queryPreview"
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
index={['foo-*']}
threshold={{ field: 'agent.hostname', value: 200 }}
threshold={{
field: 'agent.hostname',
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
}}
isDisabled={false}
/>
</TestProviders>
Expand Down Expand Up @@ -369,7 +379,12 @@ describe('PreviewQuery', () => {
idAria="queryPreview"
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
index={['foo-*']}
threshold={{ field: undefined, value: 200 }}
threshold={{
field: undefined,
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
}}
isDisabled={false}
/>
</TestProviders>
Expand All @@ -396,7 +411,12 @@ describe('PreviewQuery', () => {
idAria="queryPreview"
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
index={['foo-*']}
threshold={{ field: ' ', value: 200 }}
threshold={{
field: ' ',
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
}}
isDisabled={false}
/>
</TestProviders>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ export const initialState: State = {
showNonEqlHistogram: false,
};

export type Threshold = { field: string | undefined; value: number } | undefined;
export type Threshold =
| {
field: string | string[] | undefined;
value: number;
cardinality_field: string | undefined;
cardinality_value: number;
}
| undefined;

interface PreviewQueryProps {
dataTestSubj: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,12 @@ describe('queryPreviewReducer', () => {
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 },
threshold: {
field: 'agent.hostname',
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
},
ruleType: 'threshold',
});

Expand All @@ -347,7 +352,12 @@ describe('queryPreviewReducer', () => {
test('should set thresholdFieldExists to false if threshold field is not defined', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: { field: undefined, value: 200 },
threshold: {
field: undefined,
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
},
ruleType: 'threshold',
});

Expand All @@ -360,7 +370,12 @@ describe('queryPreviewReducer', () => {
test('should set thresholdFieldExists to false if threshold field is empty string', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: { field: ' ', value: 200 },
threshold: {
field: ' ',
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
},
ruleType: 'threshold',
});

Expand All @@ -373,7 +388,12 @@ describe('queryPreviewReducer', () => {
test('should set showNonEqlHistogram to false if ruleType is eql', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: { field: 'agent.hostname', value: 200 },
threshold: {
field: 'agent.hostname',
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
},
ruleType: 'eql',
});

Expand All @@ -385,7 +405,12 @@ describe('queryPreviewReducer', () => {
test('should set showNonEqlHistogram to true if ruleType is query', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: { field: 'agent.hostname', value: 200 },
threshold: {
field: 'agent.hostname',
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
},
ruleType: 'query',
});

Expand All @@ -397,7 +422,12 @@ describe('queryPreviewReducer', () => {
test('should set showNonEqlHistogram to true if ruleType is saved_query', () => {
const update = reducer(initialState, {
type: 'setThresholdQueryVals',
threshold: { field: 'agent.hostname', value: 200 },
threshold: {
field: 'agent.hostname',
value: 200,
cardinality_field: 'user.name',
cardinality_value: 2,
},
ruleType: 'saved_query',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type Action =
type: 'setToFrom';
};

/* eslint-disable-next-line complexity */
export const queryPreviewReducer = () => (state: State, action: Action): State => {
switch (action.type) {
case 'setQueryInfo': {
Expand Down Expand Up @@ -131,7 +132,9 @@ export const queryPreviewReducer = () => (state: State, action: Action): State =
const thresholdField =
action.threshold != null &&
action.threshold.field != null &&
action.threshold.field.trim() !== '';
((typeof action.threshold.field === 'string' && action.threshold.field.trim() !== '') ||
(Array.isArray(action.threshold.field) &&
action.threshold.field.every((field) => field.trim() !== '')));
const showNonEqlHist =
action.ruleType === 'query' ||
action.ruleType === 'saved_query' ||
Expand Down
Loading