-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[SIEM][Detection Engine] Fixes bug with timeline templates not working (
#60538) ### Summary Fixes a bug with the timeline templates not working when specifying filters. * Creates a type safe mechanism for getting StringArrays or regular strings * AddsType Script function returns to functions in the helpers file * Adds unit tests for the effected areas of code and corner cases Before this fix you would get these toaster errors if you tried to use a template name such as `host.name` in the timeline filters: <img width="677" alt="Screen Shot 2020-03-18 at 12 58 01 AM" src="https://user-images.githubusercontent.com/1151048/76934058-0bd2fc80-68b4-11ea-8dad-7c257bb81a1d.png"> After this fix it will work for you. Testing: 1) Create a timeline template that has a host.name as both a query and a filter such as this. You can give the value of the host.name any value such as placeholder. <img width="1125" alt="Screen Shot 2020-03-18 at 12 56 04 AM" src="https://user-images.githubusercontent.com/1151048/76934108-20af9000-68b4-11ea-8a11-4ba9c935506f.png"> 2) Create a signal that uses it and produces a lot of signals off of something such as all host names <img width="1054" alt="Screen Shot 2020-03-18 at 12 50 47 AM" src="https://user-images.githubusercontent.com/1151048/76934198-4f2d6b00-68b4-11ea-8ae3-6de76154cbb7.png"> 3) Ensure you select your **Timeline template** you saved by using the drop down <img width="1071" alt="Screen Shot 2020-03-18 at 12 51 21 AM" src="https://user-images.githubusercontent.com/1151048/76934281-73894780-68b4-11ea-9a2a-a0a9176f28ce.png"> 4) Once your signals have run, go to the signals page and send one of the signals for your newly crated rule which has a host name to the timeline from "View in timeline" <img width="568" alt="Screen Shot 2020-03-18 at 12 52 10 AM" src="https://user-images.githubusercontent.com/1151048/76934365-a4697c80-68b4-11ea-91a5-e0dea7e3e18f.png"> You should notice that your timeline has both the query and the filter set correctly such as this <img width="1114" alt="Screen Shot 2020-03-18 at 12 56 23 AM" src="https://user-images.githubusercontent.com/1151048/76934432-c105b480-68b4-11ea-9a82-3e8a2da19376.png"> ### Other notes All the different fields you can choose from for templates are: ``` 'host.name', 'host.hostname', 'host.domain', 'host.id', 'host.ip', 'client.ip', 'destination.ip', 'server.ip', 'source.ip', 'network.community_id', 'user.name', 'process.name', ``` And it should not work with anything outside of those. You should be able to mix and match them into different filters and queries to have a multiples of them. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
- Loading branch information
1 parent
9cb2477
commit 4ee34e6
Showing
2 changed files
with
328 additions
and
18 deletions.
There are no files selected for viewing
273 changes: 273 additions & 0 deletions
273
x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { | ||
getStringArray, | ||
replaceTemplateFieldFromQuery, | ||
replaceTemplateFieldFromMatchFilters, | ||
reformatDataProviderWithNewValue, | ||
} from './helpers'; | ||
import { mockEcsData } from '../../../../mock/mock_ecs'; | ||
import { Filter } from '../../../../../../../../../src/plugins/data/public'; | ||
import { DataProvider } from '../../../../components/timeline/data_providers/data_provider'; | ||
import { mockDataProviders } from '../../../../components/timeline/data_providers/mock/mock_data_providers'; | ||
import { cloneDeep } from 'lodash/fp'; | ||
|
||
describe('helpers', () => { | ||
let mockEcsDataClone = cloneDeep(mockEcsData); | ||
beforeEach(() => { | ||
mockEcsDataClone = cloneDeep(mockEcsData); | ||
}); | ||
describe('getStringOrStringArray', () => { | ||
test('it should correctly return a string array', () => { | ||
const value = getStringArray('x', { | ||
x: 'The nickname of the developer we all :heart:', | ||
}); | ||
expect(value).toEqual(['The nickname of the developer we all :heart:']); | ||
}); | ||
|
||
test('it should correctly return a string array with a single element', () => { | ||
const value = getStringArray('x', { | ||
x: ['The nickname of the developer we all :heart:'], | ||
}); | ||
expect(value).toEqual(['The nickname of the developer we all :heart:']); | ||
}); | ||
|
||
test('it should correctly return a string array with two elements of strings', () => { | ||
const value = getStringArray('x', { | ||
x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], | ||
}); | ||
expect(value).toEqual([ | ||
'The nickname of the developer we all :heart:', | ||
'We are all made of stars', | ||
]); | ||
}); | ||
|
||
test('it should correctly return a string array with deep elements', () => { | ||
const value = getStringArray('x.y.z', { | ||
x: { y: { z: 'zed' } }, | ||
}); | ||
expect(value).toEqual(['zed']); | ||
}); | ||
|
||
test('it should correctly return a string array with a non-existent value', () => { | ||
const value = getStringArray('non.existent', { | ||
x: { y: { z: 'zed' } }, | ||
}); | ||
expect(value).toEqual([]); | ||
}); | ||
|
||
test('it should trace an error if the value is not a string', () => { | ||
const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; | ||
const value = getStringArray('a', { a: 5 }, mockConsole); | ||
expect(value).toEqual([]); | ||
expect( | ||
mockConsole.trace | ||
).toHaveBeenCalledWith( | ||
'Data type that is not a string or string array detected:', | ||
5, | ||
'when trying to access field:', | ||
'a', | ||
'from data object of:', | ||
{ a: 5 } | ||
); | ||
}); | ||
|
||
test('it should trace an error if the value is an array of mixed values', () => { | ||
const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; | ||
const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); | ||
expect(value).toEqual([]); | ||
expect( | ||
mockConsole.trace | ||
).toHaveBeenCalledWith( | ||
'Data type that is not a string or string array detected:', | ||
['hi', 5], | ||
'when trying to access field:', | ||
'a', | ||
'from data object of:', | ||
{ a: ['hi', 5] } | ||
); | ||
}); | ||
}); | ||
|
||
describe('replaceTemplateFieldFromQuery', () => { | ||
test('given an empty query string this returns an empty query string', () => { | ||
const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); | ||
expect(replacement).toEqual(''); | ||
}); | ||
|
||
test('given a query string with spaces this returns an empty query string', () => { | ||
const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); | ||
expect(replacement).toEqual(''); | ||
}); | ||
|
||
test('it should replace a query with a template value such as apache from a mock template', () => { | ||
const replacement = replaceTemplateFieldFromQuery( | ||
'host.name: placeholdertext', | ||
mockEcsDataClone[0] | ||
); | ||
expect(replacement).toEqual('host.name: apache'); | ||
}); | ||
|
||
test('it should replace a template field with an ECS value that is not an array', () => { | ||
mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case | ||
const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); | ||
expect(replacement).toEqual('host.name: *'); | ||
}); | ||
|
||
test('it should NOT replace a query with a template value that is not part of the template fields array', () => { | ||
const replacement = replaceTemplateFieldFromQuery( | ||
'user.id: placeholdertext', | ||
mockEcsDataClone[0] | ||
); | ||
expect(replacement).toEqual('user.id: placeholdertext'); | ||
}); | ||
}); | ||
|
||
describe('replaceTemplateFieldFromMatchFilters', () => { | ||
test('given an empty query filter this will return an empty filter', () => { | ||
const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); | ||
expect(replacement).toEqual([]); | ||
}); | ||
|
||
test('given a query filter this will return that filter with the placeholder replaced', () => { | ||
const filters: Filter[] = [ | ||
{ | ||
meta: { | ||
type: 'phrase', | ||
key: 'host.name', | ||
alias: 'alias', | ||
disabled: false, | ||
negate: false, | ||
params: { query: 'Braden' }, | ||
}, | ||
query: { match_phrase: { 'host.name': 'Braden' } }, | ||
}, | ||
]; | ||
const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); | ||
const expected: Filter[] = [ | ||
{ | ||
meta: { | ||
type: 'phrase', | ||
key: 'host.name', | ||
alias: 'alias', | ||
disabled: false, | ||
negate: false, | ||
params: { query: 'apache' }, | ||
}, | ||
query: { match_phrase: { 'host.name': 'apache' } }, | ||
}, | ||
]; | ||
expect(replacement).toEqual(expected); | ||
}); | ||
|
||
test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { | ||
const filters: Filter[] = [ | ||
{ | ||
meta: { | ||
type: 'phrase', | ||
key: 'user.id', | ||
alias: 'alias', | ||
disabled: false, | ||
negate: false, | ||
params: { query: 'Evan' }, | ||
}, | ||
query: { match_phrase: { 'user.id': 'Evan' } }, | ||
}, | ||
]; | ||
const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); | ||
const expected: Filter[] = [ | ||
{ | ||
meta: { | ||
type: 'phrase', | ||
key: 'user.id', | ||
alias: 'alias', | ||
disabled: false, | ||
negate: false, | ||
params: { query: 'Evan' }, | ||
}, | ||
query: { match_phrase: { 'user.id': 'Evan' } }, | ||
}, | ||
]; | ||
expect(replacement).toEqual(expected); | ||
}); | ||
}); | ||
|
||
describe('reformatDataProviderWithNewValue', () => { | ||
test('it should replace a query with a template value such as apache from a mock data provider', () => { | ||
const mockDataProvider: DataProvider = mockDataProviders[0]; | ||
mockDataProvider.queryMatch.field = 'host.name'; | ||
mockDataProvider.id = 'Braden'; | ||
mockDataProvider.name = 'Braden'; | ||
mockDataProvider.queryMatch.value = 'Braden'; | ||
const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); | ||
expect(replacement).toEqual({ | ||
id: 'apache', | ||
name: 'apache', | ||
enabled: true, | ||
excluded: false, | ||
kqlQuery: '', | ||
queryMatch: { | ||
field: 'host.name', | ||
value: 'apache', | ||
operator: ':', | ||
displayField: undefined, | ||
displayValue: undefined, | ||
}, | ||
and: [], | ||
}); | ||
}); | ||
|
||
test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { | ||
mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case | ||
const mockDataProvider: DataProvider = mockDataProviders[0]; | ||
mockDataProvider.queryMatch.field = 'host.name'; | ||
mockDataProvider.id = 'Braden'; | ||
mockDataProvider.name = 'Braden'; | ||
mockDataProvider.queryMatch.value = 'Braden'; | ||
const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); | ||
expect(replacement).toEqual({ | ||
id: 'apache', | ||
name: 'apache', | ||
enabled: true, | ||
excluded: false, | ||
kqlQuery: '', | ||
queryMatch: { | ||
field: 'host.name', | ||
value: 'apache', | ||
operator: ':', | ||
displayField: undefined, | ||
displayValue: undefined, | ||
}, | ||
and: [], | ||
}); | ||
}); | ||
|
||
test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { | ||
const mockDataProvider: DataProvider = mockDataProviders[0]; | ||
mockDataProvider.queryMatch.field = 'user.id'; | ||
mockDataProvider.id = 'my-id'; | ||
mockDataProvider.name = 'Rebecca'; | ||
mockDataProvider.queryMatch.value = 'Rebecca'; | ||
const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); | ||
expect(replacement).toEqual({ | ||
id: 'my-id', | ||
name: 'Rebecca', | ||
enabled: true, | ||
excluded: false, | ||
kqlQuery: '', | ||
queryMatch: { | ||
field: 'user.id', | ||
value: 'Rebecca', | ||
operator: ':', | ||
displayField: undefined, | ||
displayValue: undefined, | ||
}, | ||
and: [], | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters