[Security Solution][Alerts] EQL rules fallback to @timestamp if timestamp override doesn't exist (#127989)

* EQL rules fallback to @timestamp if timestamp override doesn't exist

* Fix getEventCount test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marshall Main 2022-03-21 15:37:30 -07:00 committed by GitHub
parent a1085f4b75
commit bdecf1568e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 447 additions and 482 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { getQueryFilter, getAllFilters, buildEqlSearchRequest } from './get_query_filter';
import { getQueryFilter, getAllFilters } from './get_query_filter';
import type { Filter } from '@kbn/es-query';
import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock';
@ -1112,197 +1112,6 @@ describe('get_filter', () => {
});
});
describe('buildEqlSearchRequest', () => {
test('should build a basic request with time range', () => {
const request = buildEqlSearchRequest(
'process where true',
['testindex1', 'testindex2'],
'now-5m',
'now',
100,
undefined,
[],
undefined
);
expect(request).toEqual({
allow_no_indices: true,
index: ['testindex1', 'testindex2'],
body: {
size: 100,
query: 'process where true',
filter: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'now',
format: 'strict_date_optional_time',
},
},
},
],
},
},
fields: [
{
field: '*',
include_unmapped: true,
},
{
field: '@timestamp',
format: 'strict_date_optional_time',
},
],
},
});
});
test('should build a request with timestamp and event category overrides', () => {
const request = buildEqlSearchRequest(
'process where true',
['testindex1', 'testindex2'],
'now-5m',
'now',
100,
'event.ingested',
[],
'event.other_category'
);
expect(request).toEqual({
allow_no_indices: true,
index: ['testindex1', 'testindex2'],
body: {
event_category_field: 'event.other_category',
size: 100,
query: 'process where true',
filter: {
bool: {
filter: [
{
range: {
'event.ingested': {
gte: 'now-5m',
lte: 'now',
format: 'strict_date_optional_time',
},
},
},
],
},
},
fields: [
{
field: '*',
include_unmapped: true,
},
{
field: 'event.ingested',
format: 'strict_date_optional_time',
},
{
field: '@timestamp',
format: 'strict_date_optional_time',
},
],
},
});
});
test('should build a request with exceptions', () => {
const request = buildEqlSearchRequest(
'process where true',
['testindex1', 'testindex2'],
'now-5m',
'now',
100,
undefined,
[getExceptionListItemSchemaMock()],
undefined
);
expect(request).toEqual({
allow_no_indices: true,
index: ['testindex1', 'testindex2'],
body: {
size: 100,
query: 'process where true',
filter: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'now',
format: 'strict_date_optional_time',
},
},
},
{
bool: {
must_not: {
bool: {
should: [
{
bool: {
filter: [
{
nested: {
path: 'some.parentField',
query: {
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'some.parentField.nested.field': 'some value',
},
},
],
},
},
score_mode: 'none',
},
},
{
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'some.not.nested.field': 'some value',
},
},
],
},
},
],
},
},
],
},
},
},
},
],
},
},
fields: [
{
field: '*',
include_unmapped: true,
},
{
field: '@timestamp',
format: 'strict_date_optional_time',
},
],
},
});
});
});
describe('getAllFilters', () => {
const exceptionsFilter = {
meta: { alias: null, negate: false, disabled: false },

View file

@ -12,13 +12,9 @@ import type {
} from '@kbn/securitysolution-io-ts-list-types';
import { buildExceptionFilter } from '@kbn/securitysolution-list-utils';
import { Filter, EsQueryConfig, DataViewBase, buildEsQuery } from '@kbn/es-query';
import {
EqlSearchRequest,
QueryDslQueryContainer,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ESBoolQuery } from '../typed_json';
import { Query, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas';
import { Query, Index } from './schemas/common/schemas';
export const getQueryFilter = (
query: Query,
@ -61,76 +57,3 @@ export const getAllFilters = (filters: Filter[], exceptionFilter: Filter | undef
return [...filters];
}
};
export const buildEqlSearchRequest = (
query: string,
index: string[],
from: string,
to: string,
size: number,
timestampOverride: TimestampOverrideOrUndefined,
exceptionLists: ExceptionListItemSchema[],
eventCategoryOverride: string | undefined
): EqlSearchRequest => {
const timestamp = timestampOverride ?? '@timestamp';
const defaultTimeFields = ['@timestamp'];
const timestamps =
timestampOverride != null ? [timestampOverride, ...defaultTimeFields] : defaultTimeFields;
const docFields = timestamps.map((tstamp) => ({
field: tstamp,
format: 'strict_date_optional_time',
}));
// Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value),
// allowing us to make 1024-item chunks of exception list items.
// Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a
// very conservative value.
const exceptionFilter = buildExceptionFilter({
lists: exceptionLists,
excludeExceptions: true,
chunkSize: 1024,
});
const requestFilter: QueryDslQueryContainer[] = [
{
range: {
[timestamp]: {
gte: from,
lte: to,
format: 'strict_date_optional_time',
},
},
},
];
if (exceptionFilter !== undefined) {
requestFilter.push({
bool: {
must_not: {
bool: exceptionFilter.query?.bool,
},
},
});
}
const fields = [
{
field: '*',
include_unmapped: true,
},
...docFields,
];
return {
index,
allow_no_indices: true,
body: {
size,
query,
filter: {
bool: {
filter: requestFilter,
},
},
event_category_field: eventCategoryOverride,
fields,
},
};
};

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { buildEventsSearchQuery } from './build_events_query';
import { buildEqlSearchRequest, buildEventsSearchQuery } from './build_events_query';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
describe('create_signals', () => {
test('it builds a now-5m up to today filter', () => {
@ -29,25 +30,12 @@ describe('create_signals', () => {
filter: [
{},
{
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
],
},
},
],
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
{
@ -100,13 +88,22 @@ describe('create_signals', () => {
{},
{
bool: {
filter: [
should: [
{
range: {
'event.ingested': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
{
bool: {
should: [
filter: [
{
range: {
'event.ingested': {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
@ -115,33 +112,18 @@ describe('create_signals', () => {
},
{
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
must_not: {
exists: {
field: 'event.ingested',
},
{
bool: {
must_not: {
exists: {
field: 'event.ingested',
},
},
},
},
],
},
},
},
],
minimum_should_match: 1,
},
},
],
minimum_should_match: 1,
},
},
{
@ -204,25 +186,12 @@ describe('create_signals', () => {
filter: [
{},
{
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
],
},
},
],
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
{
@ -275,25 +244,12 @@ describe('create_signals', () => {
filter: [
{},
{
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
],
},
},
],
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
{
@ -345,25 +301,12 @@ describe('create_signals', () => {
filter: [
{},
{
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
],
},
},
],
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
{
@ -422,25 +365,12 @@ describe('create_signals', () => {
filter: [
{},
{
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
],
},
},
],
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'today',
format: 'strict_date_optional_time',
},
},
},
{
@ -536,4 +466,226 @@ describe('create_signals', () => {
},
});
});
describe('buildEqlSearchRequest', () => {
test('should build a basic request with time range', () => {
const request = buildEqlSearchRequest(
'process where true',
['testindex1', 'testindex2'],
'now-5m',
'now',
100,
undefined,
[],
undefined
);
expect(request).toEqual({
allow_no_indices: true,
index: ['testindex1', 'testindex2'],
body: {
size: 100,
query: 'process where true',
filter: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'now',
format: 'strict_date_optional_time',
},
},
},
],
},
},
fields: [
{
field: '*',
include_unmapped: true,
},
{
field: '@timestamp',
format: 'strict_date_optional_time',
},
],
},
});
});
test('should build a request with timestamp and event category overrides', () => {
const request = buildEqlSearchRequest(
'process where true',
['testindex1', 'testindex2'],
'now-5m',
'now',
100,
'event.ingested',
[],
'event.other_category'
);
expect(request).toEqual({
allow_no_indices: true,
index: ['testindex1', 'testindex2'],
body: {
event_category_field: 'event.other_category',
size: 100,
query: 'process where true',
filter: {
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
range: {
'event.ingested': {
lte: 'now',
gte: 'now-5m',
format: 'strict_date_optional_time',
},
},
},
{
bool: {
filter: [
{
range: {
'@timestamp': {
lte: 'now',
gte: 'now-5m',
format: 'strict_date_optional_time',
},
},
},
{
bool: {
must_not: {
exists: {
field: 'event.ingested',
},
},
},
},
],
},
},
],
},
},
],
},
},
fields: [
{
field: '*',
include_unmapped: true,
},
{
field: 'event.ingested',
format: 'strict_date_optional_time',
},
{
field: '@timestamp',
format: 'strict_date_optional_time',
},
],
},
});
});
test('should build a request with exceptions', () => {
const request = buildEqlSearchRequest(
'process where true',
['testindex1', 'testindex2'],
'now-5m',
'now',
100,
undefined,
[getExceptionListItemSchemaMock()],
undefined
);
expect(request).toEqual({
allow_no_indices: true,
index: ['testindex1', 'testindex2'],
body: {
size: 100,
query: 'process where true',
filter: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'now',
format: 'strict_date_optional_time',
},
},
},
{
bool: {
must_not: {
bool: {
should: [
{
bool: {
filter: [
{
nested: {
path: 'some.parentField',
query: {
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'some.parentField.nested.field': 'some value',
},
},
],
},
},
score_mode: 'none',
},
},
{
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'some.not.nested.field': 'some value',
},
},
],
},
},
],
},
},
],
},
},
},
},
],
},
},
fields: [
{
field: '*',
include_unmapped: true,
},
{
field: '@timestamp',
format: 'strict_date_optional_time',
},
],
},
});
});
});
});

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { buildExceptionFilter } from '@kbn/securitysolution-list-utils';
import { isEmpty } from 'lodash';
import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas';
interface BuildEventsSearchQuery {
aggregations?: Record<string, estypes.AggregationsAggregationContainer>;
index: string[];
@ -21,6 +22,70 @@ interface BuildEventsSearchQuery {
trackTotalHits?: boolean;
}
const buildTimeRangeFilter = ({
to,
from,
timestampOverride,
}: {
to: string;
from: string;
timestampOverride?: string;
}): estypes.QueryDslQueryContainer => {
// If the timestampOverride is provided, documents must either populate timestampOverride with a timestamp in the range
// or must NOT populate the timestampOverride field at all and `@timestamp` must fall in the range.
// If timestampOverride is not provided, we simply use `@timestamp`
return timestampOverride != null
? {
bool: {
minimum_should_match: 1,
should: [
{
range: {
[timestampOverride]: {
lte: to,
gte: from,
format: 'strict_date_optional_time',
},
},
},
{
bool: {
filter: [
{
range: {
'@timestamp': {
lte: to,
gte: from,
format: 'strict_date_optional_time',
},
},
},
{
bool: {
must_not: {
exists: {
field: timestampOverride,
},
},
},
},
],
},
},
],
},
}
: {
range: {
'@timestamp': {
lte: to,
gte: from,
format: 'strict_date_optional_time',
},
},
};
};
export const buildEventsSearchQuery = ({
aggregations,
index,
@ -41,59 +106,9 @@ export const buildEventsSearchQuery = ({
format: 'strict_date_optional_time',
}));
const rangeFilter: estypes.QueryDslQueryContainer[] =
timestampOverride != null
? [
{
range: {
[timestampOverride]: {
lte: to,
gte: from,
format: 'strict_date_optional_time',
},
},
},
{
bool: {
filter: [
{
range: {
'@timestamp': {
lte: to,
gte: from,
format: 'strict_date_optional_time',
},
},
},
{
bool: {
must_not: {
exists: {
field: timestampOverride,
},
},
},
},
],
},
},
]
: [
{
range: {
'@timestamp': {
lte: to,
gte: from,
format: 'strict_date_optional_time',
},
},
},
];
const rangeFilter = buildTimeRangeFilter({ to, from, timestampOverride });
const filterWithTime: estypes.QueryDslQueryContainer[] = [
filter,
{ bool: { filter: [{ bool: { should: [...rangeFilter], minimum_should_match: 1 } }] } },
];
const filterWithTime: estypes.QueryDslQueryContainer[] = [filter, rangeFilter];
const sort: estypes.Sort = [];
if (timestampOverride) {
@ -151,3 +166,66 @@ export const buildEventsSearchQuery = ({
}
return searchQuery;
};
export const buildEqlSearchRequest = (
query: string,
index: string[],
from: string,
to: string,
size: number,
timestampOverride: TimestampOverrideOrUndefined,
exceptionLists: ExceptionListItemSchema[],
eventCategoryOverride: string | undefined
): estypes.EqlSearchRequest => {
const defaultTimeFields = ['@timestamp'];
const timestamps =
timestampOverride != null ? [timestampOverride, ...defaultTimeFields] : defaultTimeFields;
const docFields = timestamps.map((tstamp) => ({
field: tstamp,
format: 'strict_date_optional_time',
}));
// Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value),
// allowing us to make 1024-item chunks of exception list items.
// Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a
// very conservative value.
const exceptionFilter = buildExceptionFilter({
lists: exceptionLists,
excludeExceptions: true,
chunkSize: 1024,
});
const rangeFilter = buildTimeRangeFilter({ to, from, timestampOverride });
const requestFilter: estypes.QueryDslQueryContainer[] = [rangeFilter];
if (exceptionFilter !== undefined) {
requestFilter.push({
bool: {
must_not: {
bool: exceptionFilter.query?.bool,
},
},
});
}
const fields = [
{
field: '*',
include_unmapped: true,
},
...docFields,
];
return {
index,
allow_no_indices: true,
body: {
size,
query,
filter: {
bool: {
filter: requestFilter,
},
},
event_category_field: eventCategoryOverride,
fields,
},
};
};

View file

@ -13,7 +13,7 @@ import {
AlertInstanceState,
AlertServices,
} from '../../../../../../alerting/server';
import { buildEqlSearchRequest } from '../../../../../common/detection_engine/get_query_filter';
import { buildEqlSearchRequest } from '../build_events_query';
import { hasLargeValueItem } from '../../../../../common/detection_engine/utils';
import { isOutdated } from '../../migrations/helpers';
import { getIndexVersion } from '../../routes/index/get_index_version';

View file

@ -33,25 +33,12 @@ describe('getEventCount', () => {
filter: [
{ bool: { must: [], filter: [], should: [], must_not: [] } },
{
bool: {
filter: [
{
bool: {
should: [
{
range: {
'@timestamp': {
lte: '2022-01-14T05:00:00.000Z',
gte: '2022-01-13T05:00:00.000Z',
format: 'strict_date_optional_time',
},
},
},
],
minimum_should_match: 1,
},
},
],
range: {
'@timestamp': {
lte: '2022-01-14T05:00:00.000Z',
gte: '2022-01-13T05:00:00.000Z',
format: 'strict_date_optional_time',
},
},
},
{ match_all: {} },
@ -84,40 +71,34 @@ describe('getEventCount', () => {
{ bool: { must: [], filter: [], should: [], must_not: [] } },
{
bool: {
filter: [
should: [
{
range: {
'event.ingested': {
lte: '2022-01-14T05:00:00.000Z',
gte: '2022-01-13T05:00:00.000Z',
format: 'strict_date_optional_time',
},
},
},
{
bool: {
should: [
filter: [
{
range: {
'event.ingested': {
'@timestamp': {
lte: '2022-01-14T05:00:00.000Z',
gte: '2022-01-13T05:00:00.000Z',
format: 'strict_date_optional_time',
},
},
},
{
bool: {
filter: [
{
range: {
'@timestamp': {
lte: '2022-01-14T05:00:00.000Z',
gte: '2022-01-13T05:00:00.000Z',
format: 'strict_date_optional_time',
},
},
},
{ bool: { must_not: { exists: { field: 'event.ingested' } } } },
],
},
},
{ bool: { must_not: { exists: { field: 'event.ingested' } } } },
],
minimum_should_match: 1,
},
},
],
minimum_should_match: 1,
},
},
{ match_all: {} },

View file

@ -279,6 +279,28 @@ export default ({ getService }: FtrProviderContext) => {
expect(signalsOrderedByEventId.length).equal(2);
});
it('should generate 2 signals when timestamp override does not exist', async () => {
const rule: EqlCreateSchema = {
...getEqlRuleForSignalTesting(['myfa*']),
timestamp_override: 'event.fakeingestfield',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccessOrStatus(
supertest,
log,
id,
RuleExecutionStatus['partial failure']
);
await sleep(5000);
await waitForSignalsToBePresent(supertest, log, 2, [id]);
const signalsResponse = await getSignalsByIds(supertest, log, [id, id]);
const signals = signalsResponse.hits.hits.map((hit) => hit._source);
const signalsOrderedByEventId = orderBy(signals, 'signal.parent.id', 'asc');
expect(signalsOrderedByEventId.length).equal(2);
});
});
});