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

[Detections][EQL] EQL rule execution in detection engine #77419

Merged
merged 31 commits into from
Sep 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6cf620f
First draft of EQL rules in detection engine
marshallmain Sep 14, 2020
82ae0c2
Reorganize functions to separate files
marshallmain Sep 15, 2020
9b450bf
Start adding eventCategoryOverride option for EQL rules
marshallmain Sep 18, 2020
d3fce8a
Add building block alerts for each event within sequence
marshallmain Sep 21, 2020
ad9fe1b
Merge branch 'master' into eql-rules
marshallmain Sep 21, 2020
922a21e
Use eql instead of eql_query for rule type
marshallmain Sep 21, 2020
ecedb3f
Remove unused imports
marshallmain Sep 21, 2020
3e45bc8
Fix tests
marshallmain Sep 21, 2020
4faa592
Add basic tests for buildEqlSearchRequest
marshallmain Sep 22, 2020
f9e26b0
Add rulesSchema tests for eql
marshallmain Sep 22, 2020
3fd19f0
Add buildSignalFromSequence test
marshallmain Sep 22, 2020
bcdde62
Add threat rule fields to buildRuleWithoutOverrides
marshallmain Sep 22, 2020
162256a
Fix buildSignalFromSequence typecheck error
marshallmain Sep 22, 2020
3f89fb9
Add more tests
marshallmain Sep 22, 2020
8b00aa7
Add tests for wrapBuildingBlock and generateSignalId
marshallmain Sep 22, 2020
2657188
Merge branch 'master' into eql-rules
marshallmain Sep 23, 2020
1011f07
Use isEqlRule function and fix import error
marshallmain Sep 23, 2020
cf0f9fb
delete frank
marshallmain Sep 23, 2020
1d680a7
Move sequence interface to types.ts
marshallmain Sep 23, 2020
5ebe577
Fix import
marshallmain Sep 23, 2020
f445d87
Remove EQL execution placeholder, add back language to eql rule type
marshallmain Sep 23, 2020
1c5afd9
allow no indices for eql search
marshallmain Sep 23, 2020
506a3d6
Fix unit tests for language update
marshallmain Sep 23, 2020
aacc605
Fix buildEqlSearchRequest tests
marshallmain Sep 23, 2020
19cf9e2
Replace signal.child with signal.group
marshallmain Sep 23, 2020
b42cf73
remove unused import
marshallmain Sep 23, 2020
1942bf9
Move sequence signal group building to separate testable function
marshallmain Sep 23, 2020
22383a9
Merge branch 'master' into eql-rules
elasticmachine Sep 24, 2020
8142b11
Merge branch 'master' into eql-rules
marshallmain Sep 24, 2020
45ecb42
Unbork the merge conflict resolution
marshallmain Sep 24, 2020
9a6e908
Merge branch 'master' into eql-rules
elasticmachine Sep 25, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { getQueryFilter, buildExceptionFilter } from './get_query_filter';
import { getQueryFilter, buildExceptionFilter, buildEqlSearchRequest } from './get_query_filter';
import { Filter, EsQueryConfig } from 'src/plugins/data/public';
import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock';

Expand Down Expand Up @@ -1085,4 +1085,138 @@ 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({
method: 'POST',
path: `/testindex1,testindex2/_eql/search?allow_no_indices=true`,
body: {
size: 100,
query: 'process where true',
filter: {
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'now',
},
},
},
},
});
});

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({
method: 'POST',
path: `/testindex1,testindex2/_eql/search?allow_no_indices=true`,
event_category_field: 'event.other_category',
body: {
size: 100,
query: 'process where true',
filter: {
range: {
'event.ingested': {
gte: 'now-5m',
lte: 'now',
},
},
},
},
});
});

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({
method: 'POST',
path: `/testindex1,testindex2/_eql/search?allow_no_indices=true`,
body: {
size: 100,
query: 'process where true',
filter: {
range: {
'@timestamp': {
gte: 'now-5m',
lte: 'now',
},
},
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',
},
},
],
},
},
],
},
},
],
},
},
},
},
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import {
CreateExceptionListItemSchema,
} from '../../../lists/common/schemas';
import { buildExceptionListQueries } from './build_exceptions_query';
import { Query as QueryString, Language, Index } from './schemas/common/schemas';
import {
Query as QueryString,
Language,
Index,
TimestampOverrideOrUndefined,
} from './schemas/common/schemas';

export const getQueryFilter = (
query: QueryString,
Expand Down Expand Up @@ -67,6 +72,78 @@ export const getQueryFilter = (
return buildEsQuery(indexPattern, initialQuery, enabledFilters, config);
};

interface EqlSearchRequest {
method: string;
path: string;
body: object;
event_category_field?: string;
}

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 indexPattern: IIndexPattern = {
fields: [],
title: index.join(),
};
const config: EsQueryConfig = {
allowLeadingWildcards: true,
queryStringOptions: { analyze_wildcard: true },
ignoreFilterIfFieldNotInIndex: false,
dateFormatTZ: 'Zulu',
};
const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists: exceptionLists });
let exceptionFilter: Filter | undefined;
if (exceptionQueries.length > 0) {
// 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.
exceptionFilter = buildExceptionFilter(exceptionQueries, indexPattern, config, true, 1024);
}
const indexString = index.join();
const baseRequest = {
method: 'POST',
path: `/${indexString}/_eql/search?allow_no_indices=true`,
body: {
size,
query,
filter: {
range: {
[timestamp]: {
gte: from,
lte: to,
},
},
bool:
exceptionFilter !== undefined
? {
must_not: {
bool: exceptionFilter?.query.bool,
},
}
: undefined,
},
},
};
if (eventCategoryOverride) {
return {
...baseRequest,
event_category_field: eventCategoryOverride,
};
} else {
return baseRequest;
}
};

export const buildExceptionFilter = (
exceptionQueries: Query[],
indexPattern: IIndexPattern,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export type Enabled = t.TypeOf<typeof enabled>;
export const enabledOrUndefined = t.union([enabled, t.undefined]);
export type EnabledOrUndefined = t.TypeOf<typeof enabledOrUndefined>;

export const event_category_override = t.string;
export type EventCategoryOverride = t.TypeOf<typeof event_category_override>;

export const eventCategoryOverrideOrUndefined = t.union([event_category_override, t.undefined]);
export type EventCategoryOverrideOrUndefined = t.TypeOf<typeof eventCategoryOverrideOrUndefined>;

export const false_positives = t.array(t.string);
export type FalsePositives = t.TypeOf<typeof false_positives>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
Author,
RiskScoreMapping,
SeverityMapping,
event_category_override,
} from '../common/schemas';
import {
threat_index,
Expand Down Expand Up @@ -96,6 +97,7 @@ export const addPrepackagedRulesSchema = t.intersection([
author: DefaultStringArray, // defaults to empty array of strings if not set during decode
building_block_type, // defaults to undefined if not set during decode
enabled: DefaultBooleanFalse, // defaults to false if not set during decode
event_category_override, // defaults to "undefined" if not set during decode
false_positives: DefaultStringArray, // defaults to empty string array if not set during decode
filters, // defaults to undefined if not set during decode
from: DefaultFromString, // defaults to "now-6m" if not set during decode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
Author,
RiskScoreMapping,
SeverityMapping,
event_category_override,
} from '../common/schemas';
import {
threat_index,
Expand Down Expand Up @@ -88,6 +89,7 @@ export const createRulesSchema = t.intersection([
author: DefaultStringArray, // defaults to empty array of strings if not set during decode
building_block_type, // defaults to undefined if not set during decode
enabled: DefaultBooleanTrue, // defaults to true if not set during decode
event_category_override, // defaults to "undefined" if not set during decode
false_positives: DefaultStringArray, // defaults to empty string array if not set during decode
filters, // defaults to undefined if not set during decode
from: DefaultFromString, // defaults to "now-6m" if not set during decode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
Author,
RiskScoreMapping,
SeverityMapping,
event_category_override,
} from '../common/schemas';
import {
threat_index,
Expand Down Expand Up @@ -107,6 +108,7 @@ export const importRulesSchema = t.intersection([
author: DefaultStringArray, // defaults to empty array of strings if not set during decode
building_block_type, // defaults to undefined if not set during decode
enabled: DefaultBooleanTrue, // defaults to true if not set during decode
event_category_override, // defaults to "undefined" if not set during decode
false_positives: DefaultStringArray, // defaults to empty string array if not set during decode
filters, // defaults to undefined if not set during decode
from: DefaultFromString, // defaults to "now-6m" if not set during decode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
timestamp_override,
risk_score_mapping,
severity_mapping,
event_category_override,
} from '../common/schemas';
import { listArrayOrUndefined } from '../types/lists';

Expand All @@ -65,6 +66,7 @@ export const patchRulesSchema = t.exact(
actions,
anomaly_threshold,
enabled,
event_category_override,
false_positives,
filters,
from,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
Author,
RiskScoreMapping,
SeverityMapping,
event_category_override,
} from '../common/schemas';

import {
Expand Down Expand Up @@ -90,6 +91,7 @@ export const updateRulesSchema = t.intersection([
author: DefaultStringArray, // defaults to empty array of strings if not set during decode
building_block_type, // defaults to undefined if not set during decode
enabled: DefaultBooleanTrue, // defaults to true if not set during decode
event_category_override,
false_positives: DefaultStringArray, // defaults to empty string array if not set during decode
filters, // defaults to undefined if not set during decode
from: DefaultFromString, // defaults to "now-6m" if not set during decode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem
severity: 'high',
severity_mapping: [],
updated_by: 'elastic_kibana',
tags: [],
tags: ['some fake tag 1', 'some fake tag 2'],
to: 'now',
type: 'query',
threat: [],
Expand All @@ -61,7 +61,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem
status_date: '2020-02-22T16:47:50.047Z',
last_success_at: '2020-02-22T16:47:50.047Z',
last_success_message: 'succeeded',
output_index: '.siem-signals-hassanabad-frank-default',
output_index: '.siem-signals-default',
max_signals: 100,
risk_score: 55,
risk_score_mapping: [],
Expand Down Expand Up @@ -110,3 +110,12 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R
],
};
};

export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => {
return {
...getRulesSchemaMock(anchorDate),
language: 'eql',
type: 'eql',
query: 'process where true',
};
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Another way I have seen these written as has been like so:

export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => {
  const { language, ...noLanguage } = getRulesSchemaMock(anchorDate);
  return {
    ...noLanguage,
    type: 'eql',
    query: 'process where true',
  };
};

No changes asked, just pointing out another way if you wanted to avoid the delete and the assignments.

Loading