Skip to content

Commit

Permalink
[Security Solutions] Fixes exception lists to be able to filter on os…
Browse files Browse the repository at this point in the history
… type (#106494)

## Summary

Fixes #102613, and targets `7.14.0` as a blocker/critical

Previously we never fully finished the plumbing for using the `os_types` (operating system type) in the exception lists to be able to filter out values based on this type. With the endpoint exceptions now having specific selections for os_type we have to filter it with exceptions and basically make it work.

Some caveats is that the endpoints utilize `host.os.name.casless` for filtering against os_type, while agents such as auditbeat, winlogbeat, etc... use `host.os.type`. Really `host.os.type` is the correct ECS field to use, but to retain compatibility with the current version of endpoint agents I support both in one query to where if either of these two matches, then that will trigger the exceptions.

* Adds e2e tests
* Enhances the e2e tooling to do endpoint exception testing with `os_types`.
* Adds the logic to handle os_type
* Updates the unit tests

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
  • Loading branch information
FrankHassanabad authored Jul 22, 2021
1 parent cd667d0 commit 0a5c96b
Show file tree
Hide file tree
Showing 15 changed files with 1,427 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
entriesMatch,
entriesMatchAny,
entriesNested,
OsTypeArray,
} from '@kbn/securitysolution-io-ts-list-types';

import { hasLargeValueList } from '../has_large_value_list';
Expand Down Expand Up @@ -69,26 +70,87 @@ export const chunkExceptions = (
return chunk(chunkSize, exceptions);
};

export const buildExceptionItemFilter = (
exceptionItem: ExceptionItemSansLargeValueLists
): BooleanFilter | NestedFilter => {
const { entries } = exceptionItem;
/**
* Transforms the os_type into a regular filter as if the user had created it
* from the fields for the next state of transforms which will create the elastic filters
* from it.
*
* Note: We use two types of fields, the "host.os.type" and "host.os.name.caseless"
* The endpoint/endgame agent has been using "host.os.name.caseless" as the same value as the ECS
* value of "host.os.type" where the auditbeat, winlogbeat, etc... (other agents) are all using
* "host.os.type". In order to be compatible with both, I create an "OR" between these two data types
* where if either has a match then we will exclude it as part of the match. This should also be
* forwards compatible for endpoints/endgame agents when/if they upgrade to using "host.os.type"
* rather than using "host.os.name.caseless" values.
*
* Also we create another "OR" from the osType names so that if there are multiples such as ['windows', 'linux']
* this will exclude anything with either 'windows' or with 'linux'
* @param osTypes The os_type array from the REST interface that is an array such as ['windows', 'linux']
* @param entries The entries to join the OR's with before the elastic filter change out
*/
export const transformOsType = (
osTypes: OsTypeArray,
entries: NonListEntry[]
): NonListEntry[][] => {
const hostTypeTransformed = osTypes.map<NonListEntry[]>((osType) => {
return [
{ field: 'host.os.type', operator: 'included', type: 'match', value: osType },
...entries,
];
});
const caseLessTransformed = osTypes.map<NonListEntry[]>((osType) => {
return [
{ field: 'host.os.name.caseless', operator: 'included', type: 'match', value: osType },
...entries,
];
});
return [...hostTypeTransformed, ...caseLessTransformed];
};

if (entries.length === 1) {
return createInnerAndClauses(entries[0]);
} else {
/**
* This builds an exception item filter with the os type
* @param osTypes The os_type array from the REST interface that is an array such as ['windows', 'linux']
* @param entries The entries to join the OR's with before the elastic filter change out
*/
export const buildExceptionItemFilterWithOsType = (
osTypes: OsTypeArray,
entries: NonListEntry[]
): BooleanFilter[] => {
const entriesWithOsTypes = transformOsType(osTypes, entries);
return entriesWithOsTypes.map((entryWithOsType) => {
return {
bool: {
filter: entries.map((entry) => createInnerAndClauses(entry)),
filter: entryWithOsType.map((entry) => createInnerAndClauses(entry)),
},
};
});
};

export const buildExceptionItemFilter = (
exceptionItem: ExceptionItemSansLargeValueLists
): Array<BooleanFilter | NestedFilter> => {
const { entries, os_types: osTypes } = exceptionItem;
if (osTypes != null && osTypes.length > 0) {
return buildExceptionItemFilterWithOsType(osTypes, entries);
} else {
if (entries.length === 1) {
return [createInnerAndClauses(entries[0])];
} else {
return [
{
bool: {
filter: entries.map((entry) => createInnerAndClauses(entry)),
},
},
];
}
}
};

export const createOrClauses = (
exceptionItems: ExceptionItemSansLargeValueLists[]
): Array<BooleanFilter | NestedFilter> => {
return exceptionItems.map((exceptionItem) => buildExceptionItemFilter(exceptionItem));
return exceptionItems.flatMap((exceptionItem) => buildExceptionItemFilter(exceptionItem));
};

export const buildExceptionFilter = ({
Expand Down
171 changes: 86 additions & 85 deletions x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,114 +611,115 @@ describe('build_exceptions_filter', () => {
getEntryExistsExcludedMock(),
],
});

expect(exceptionItemFilter).toEqual({
bool: {
filter: [
{
nested: {
path: 'parent.field',
query: {
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'parent.field.host.name': 'some host name',
expect(exceptionItemFilter).toEqual([
{
bool: {
filter: [
{
nested: {
path: 'parent.field',
query: {
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'parent.field.host.name': 'some host name',
},
},
},
],
],
},
},
},
{
bool: {
must_not: {
bool: {
minimum_should_match: 1,
should: [
{
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'parent.field.host.name': 'some host name',
{
bool: {
must_not: {
bool: {
minimum_should_match: 1,
should: [
{
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'parent.field.host.name': 'some host name',
},
},
},
],
],
},
},
},
{
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'parent.field.host.name': 'some other host name',
{
bool: {
minimum_should_match: 1,
should: [
{
match_phrase: {
'parent.field.host.name': 'some other host name',
},
},
},
],
],
},
},
},
],
],
},
},
},
},
},
{
bool: {
minimum_should_match: 1,
should: [{ exists: { field: 'parent.field.host.name' } }],
{
bool: {
minimum_should_match: 1,
should: [{ exists: { field: 'parent.field.host.name' } }],
},
},
},
],
],
},
},
score_mode: 'none',
},
score_mode: 'none',
},
},
{
bool: {
minimum_should_match: 1,
should: [
{
bool: {
minimum_should_match: 1,
should: [{ match_phrase: { 'host.name': 'some "host" name' } }],
{
bool: {
minimum_should_match: 1,
should: [
{
bool: {
minimum_should_match: 1,
should: [{ match_phrase: { 'host.name': 'some "host" name' } }],
},
},
},
{
{
bool: {
minimum_should_match: 1,
should: [{ match_phrase: { 'host.name': 'some other host name' } }],
},
},
],
},
},
{
bool: {
must_not: {
bool: {
minimum_should_match: 1,
should: [{ match_phrase: { 'host.name': 'some other host name' } }],
should: [{ match_phrase: { 'host.name': 'some host name' } }],
},
},
],
},
},
{
bool: {
must_not: {
bool: {
minimum_should_match: 1,
should: [{ match_phrase: { 'host.name': 'some host name' } }],
},
},
},
},
{
bool: {
must_not: {
bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] },
{
bool: {
must_not: {
bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] },
},
},
},
},
],
],
},
},
});
]);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const getExceptionListItemSchemaMock = (
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
os_types: ['linux'],
os_types: [],
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: TIE_BREAKER,
type: ITEM_TYPE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ describe('Exception helpers', () => {
meta: {},
name: 'some name',
namespace_type: 'single',
os_types: ['linux'],
os_types: [],
tags: ['user added string for a tag', 'malware'],
type: 'simple',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('ExceptionDetails', () => {
});

test('it renders the operating system if one is specified in the exception item', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ExceptionDetails
Expand All @@ -173,7 +173,7 @@ describe('ExceptionDetails', () => {
});

test('it renders the exception item creator', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ExceptionDetails
Expand All @@ -191,7 +191,7 @@ describe('ExceptionDetails', () => {
});

test('it renders the exception item creation timestamp', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ExceptionDetails
Expand All @@ -207,7 +207,7 @@ describe('ExceptionDetails', () => {
});

test('it renders the description if one is included on the exception item', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ExceptionDetails
Expand All @@ -223,7 +223,7 @@ describe('ExceptionDetails', () => {
});

test('it renders with Name and Modified info when showName and showModified props are true', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
exceptionItem.comments = [];

const wrapper = mount(
Expand Down
Loading

0 comments on commit 0a5c96b

Please sign in to comment.