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][Detection Engine] Fix importing rules with multiple types of exception lists #198868

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('getExceptionListFilter', () => {
savedObjectTypes: ['exception-list-agnostic'],
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.list_type: list) AND exception-list-agnostic.attributes.name: "Sample Endpoint Exception List"'
'(exception-list-agnostic.attributes.list_type: list) AND (exception-list-agnostic.attributes.name: "Sample Endpoint Exception List")'
);
});

Expand All @@ -40,7 +40,7 @@ describe('getExceptionListFilter', () => {
savedObjectTypes: ['exception-list'],
});
expect(filter).toEqual(
'(exception-list.attributes.list_type: list) AND exception-list.attributes.name: "Sample Endpoint Exception List"'
'(exception-list.attributes.list_type: list) AND (exception-list.attributes.name: "Sample Endpoint Exception List")'
);
});

Expand All @@ -60,7 +60,7 @@ describe('getExceptionListFilter', () => {
savedObjectTypes: ['exception-list-agnostic', 'exception-list'],
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.list_type: list OR exception-list.attributes.list_type: list) AND exception-list-agnostic.attributes.name: "Sample Endpoint Exception List"'
Copy link
Contributor

Choose a reason for hiding this comment

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

None of these examples demonstrate the need for the added parentheses; they all look to be logically equivalent. Is it possible to add a test for the situation causing the bug?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The unit tests don't actually test the query logic so an extra test would be a near duplicate. I modified this one though to show the importance more clearly.

'(exception-list-agnostic.attributes.list_type: list OR exception-list.attributes.list_type: list) AND (exception-list-agnostic.attributes.name: "Sample Endpoint Exception List")'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ export const getExceptionListFilter = ({
.join(' OR ');

if (filter != null) {
return `(${listTypesFilter}) AND ${filter}`;
return `(${listTypesFilter}) AND (${filter})`;
} else return `(${listTypesFilter})`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('find_all_exception_list_item_types', () => {

expect(findExceptionList).toHaveBeenCalledWith({
filter: 'exception-list-agnostic.attributes.list_id:(1)',
namespaceType: ['agnostic'],
namespaceType: ['single', 'agnostic'],
page: undefined,
perPage: 1000,
savedObjectsClient,
Expand All @@ -74,7 +74,7 @@ describe('find_all_exception_list_item_types', () => {

expect(findExceptionList).toHaveBeenCalledWith({
filter: 'exception-list.attributes.list_id:(1)',
namespaceType: ['single'],
namespaceType: ['single', 'agnostic'],
page: undefined,
perPage: 1000,
savedObjectsClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,57 +52,40 @@ export const findAllListTypes = async (
nonAgnosticListItems: ExceptionListQueryInfo[],
savedObjectsClient: SavedObjectsClientContract
): Promise<FoundExceptionListSchema | null> => {
// Agnostic filter
const agnosticFilter = getListFilter({
namespaceType: 'agnostic',
objects: agnosticListItems,
});

// Non-agnostic filter
const nonAgnosticFilter = getListFilter({
namespaceType: 'single',
objects: nonAgnosticListItems,
});

if (!agnosticListItems.length && !nonAgnosticListItems.length) {
return null;
} else if (agnosticListItems.length && !nonAgnosticListItems.length) {
return findExceptionList({
filter: agnosticFilter,
namespaceType: ['agnostic'],
page: undefined,
perPage: CHUNK_PARSED_OBJECT_SIZE,
pit: undefined,
savedObjectsClient,
searchAfter: undefined,
sortField: undefined,
sortOrder: undefined,
});
} else if (!agnosticListItems.length && nonAgnosticListItems.length) {
return findExceptionList({
filter: nonAgnosticFilter,
namespaceType: ['single'],
page: undefined,
perPage: CHUNK_PARSED_OBJECT_SIZE,
pit: undefined,
savedObjectsClient,
searchAfter: undefined,
sortField: undefined,
sortOrder: undefined,
});
} else {
return findExceptionList({
filter: `${agnosticFilter} OR ${nonAgnosticFilter}`,
namespaceType: ['single', 'agnostic'],
page: undefined,
perPage: CHUNK_PARSED_OBJECT_SIZE,
pit: undefined,
savedObjectsClient,
searchAfter: undefined,
sortField: undefined,
sortOrder: undefined,
});
}

const filters: string[] = [];
if (agnosticListItems.length > 0) {
filters.push(
getListFilter({
namespaceType: 'agnostic',
objects: agnosticListItems,
})
);
}

if (nonAgnosticListItems.length > 0) {
filters.push(
getListFilter({
namespaceType: 'single',
objects: nonAgnosticListItems,
})
);
}

return findExceptionList({
filter: filters.join(' OR '),
namespaceType: ['single', 'agnostic'],
page: undefined,
perPage: CHUNK_PARSED_OBJECT_SIZE,
pit: undefined,
savedObjectsClient,
searchAfter: undefined,
sortField: undefined,
sortOrder: undefined,
});
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,58 @@ export default ({ getService }: FtrProviderContext): void => {
});
});

it('should be able to import a rule with both single space and space agnostic exception lists', async () => {
const ndjson = combineToNdJson(
getCustomQueryRuleParams({
exceptions_list: [
{
id: 'agnostic',
list_id: 'test_list_agnostic_id',
type: 'detection',
namespace_type: 'agnostic',
},
{
id: 'single',
list_id: 'test_list_id',
type: 'rule_default',
namespace_type: 'single',
},
],
}),
{ ...getImportExceptionsListSchemaMock('test_list_id'), type: 'rule_default' },
getImportExceptionsListItemNewerVersionSchemaMock('test_item_id', 'test_list_id'),
{
...getImportExceptionsListSchemaMock('test_list_agnostic_id'),
type: 'detection',
namespace_type: 'agnostic',
},
{
...getImportExceptionsListItemNewerVersionSchemaMock(
'test_item_id',
'test_list_agnostic_id'
),
namespace_type: 'agnostic',
}
);

const { body } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach('file', Buffer.from(ndjson), 'rules.ndjson')
.expect(200);

expect(body).toMatchObject({
success: true,
success_count: 1,
rules_count: 1,
errors: [],
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 2,
});
});

it('should only remove non existent exception list references from rule', async () => {
// create an exception list
const { body: exceptionBody } = await supertest
Expand Down