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][Endpoint] Update list api summary endpoint to use filter #123476

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
* Side Public License, v 1.
*/

import { ID, LIST_ID, NAMESPACE_TYPE } from '../../constants/index.mock';
import { FILTER, ID, LIST_ID, NAMESPACE_TYPE } from '../../constants/index.mock';

import { SummaryExceptionListSchema } from '.';

export const getSummaryExceptionListSchemaMock = (): SummaryExceptionListSchema => ({
filter: FILTER,
id: ID,
list_id: LIST_ID,
namespace_type: NAMESPACE_TYPE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ describe('summary_exception_list_schema', () => {
expect(message.schema).toEqual(payload);
});

test('it should accept an undefined for "filter"', () => {
const payload = getSummaryExceptionListSchemaMock();
delete payload.filter;
const decoded = summaryExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should accept an undefined for "id"', () => {
const payload = getSummaryExceptionListSchemaMock();
delete payload.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import * as t from 'io-ts';
import { NamespaceType } from '../../common/default_namespace';
import { RequiredKeepUndefined } from '../../common/required_keep_undefined';
import { id } from '../../common/id';
import { filter, Filter } from '../../common/filter';
import { list_id } from '../../common/list_id';
import { namespace_type } from '../../common/namespace_type';

export const summaryExceptionListSchema = t.exact(
t.partial({
filter,
id,
list_id,
namespace_type, // defaults to 'single' if not set during decode
Expand All @@ -30,4 +32,5 @@ export type SummaryExceptionListSchemaDecoded = Omit<
'namespace_type'
> & {
namespace_type: NamespaceType;
filter: Filter;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ExceptionListSummarySchema } from '@kbn/securitysolution-io-ts-list-types';

export const getSummaryExceptionListSchemaMock = (
overrides?: Partial<ExceptionListSummarySchema>
): ExceptionListSummarySchema => {
return {
linux: 0,
macos: 0,
total: 0,
windows: 0,
...(overrides || {}),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ export const summaryExceptionListRoute = (router: ListsPluginRouter): void => {
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
const { id, list_id: listId, namespace_type: namespaceType } = request.query;
const { id, list_id: listId, namespace_type: namespaceType, filter } = request.query;
const exceptionLists = getExceptionListClient(context);
if (id != null || listId != null) {
const exceptionListSummary = await exceptionLists.getExceptionListSummary({
filter,
id,
listId,
namespaceType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ describe('exception_list_client', () => {
'getExceptionListSummary',
(): ReturnType<ExceptionListClient['getExceptionListSummary']> => {
return exceptionListClient.getExceptionListSummary({
filter: undefined,
id: '1',
listId: '1',
namespaceType: 'agnostic',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,14 @@ export class ExceptionListClient {

/**
* Fetch an exception list parent container
* @params filter {sting | undefined} kql "filter" expression
* @params listId {string | undefined} the "list_id" of an exception list
* @params id {string | undefined} the "id" of an exception list
* @params namespaceType {string | undefined} saved object namespace (single | agnostic)
* @return {ExceptionListSummarySchema | null} summary of exception list item os types
*/
public getExceptionListSummary = async ({
filter,
listId,
id,
namespaceType,
Expand All @@ -140,6 +142,7 @@ export class ExceptionListClient {
await this.serverExtensionsClient.pipeRun(
'exceptionsListPreSummary',
{
filter,
id,
listId,
namespaceType,
Expand All @@ -148,7 +151,7 @@ export class ExceptionListClient {
);
}

return getExceptionListSummary({ id, listId, namespaceType, savedObjectsClient });
return getExceptionListSummary({ filter, id, listId, namespaceType, savedObjectsClient });
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface GetExceptionListOptions {
}

export interface GetExceptionListSummaryOptions {
filter: FilterOrUndefined;
listId: ListIdOrUndefined;
id: IdOrUndefined;
namespaceType: NamespaceType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants';
import { ExceptionListSummarySchema } from '@kbn/securitysolution-io-ts-list-types';

import type { SavedObjectsClientContract } from '../../../../../../src/core/server';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';

import { getExceptionListSummary } from './get_exception_list_summary';

describe('get_exception_list_summary', () => {
describe('getExceptionListSummary', () => {
let savedObjectsClient: jest.Mocked<SavedObjectsClientContract>;

beforeEach(() => {
savedObjectsClient = savedObjectsClientMock.create();
});

test('it should aggregate items if not host isolation exception artifact', async () => {
const savedObject = {
aggregations: {
by_os: {
buckets: [
{ doc_count: 2, key: 'linux' },
{ doc_count: 3, key: 'macos' },
{ doc_count: 5, key: 'windows' },
],
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
},
},
page: 1,
per_page: 0,
saved_objects: [],
total: 10,
};
savedObjectsClient.find.mockResolvedValue(savedObject);

const summary = (await getExceptionListSummary({
filter: undefined,
id: undefined,
listId: '',
namespaceType: 'agnostic',
savedObjectsClient,
})) as ExceptionListSummarySchema;

expect(summary.total).toEqual(10);
});

test('it should NOT aggregate items if host isolation exception artifact', async () => {
const savedObject = {
aggregations: {
by_os: {
buckets: [
{ doc_count: 3, key: 'linux' },
{ doc_count: 3, key: 'macos' },
{ doc_count: 3, key: 'windows' },
],
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
},
},
page: 1,
per_page: 0,
saved_objects: [],
total: 3,
};
savedObjectsClient.find.mockResolvedValue(savedObject);

const summary = (await getExceptionListSummary({
filter: undefined,
id: undefined,
listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
namespaceType: 'agnostic',
savedObjectsClient,
})) as ExceptionListSummarySchema;

expect(summary.total).toEqual(3);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import type {
ExceptionListSummarySchema,
FilterOrUndefined,
IdOrUndefined,
ListIdOrUndefined,
NamespaceType,
Expand All @@ -20,6 +21,7 @@ import {
import { ExceptionListSoSchema } from '../../schemas/saved_objects';

interface GetExceptionListSummaryOptions {
filter: FilterOrUndefined;
id: IdOrUndefined;
listId: ListIdOrUndefined;
savedObjectsClient: SavedObjectsClientContract;
Expand All @@ -37,6 +39,7 @@ interface ByOsAggType {
}

export const getExceptionListSummary = async ({
filter,
id,
listId,
savedObjectsClient,
Expand All @@ -59,6 +62,10 @@ export const getExceptionListSummary = async ({
}
}

// only pick the items in the list and not the list definition
const itemTypeFilter = `${savedObjectType}.attributes.type: "simple"`;
const adjustedFilter = filter ? `(${filter}) AND ${itemTypeFilter}` : itemTypeFilter;

const savedObject = await savedObjectsClient.find<ExceptionListSoSchema, ByOsAggType>({
aggs: {
by_os: {
Expand All @@ -67,7 +74,7 @@ export const getExceptionListSummary = async ({
},
},
},
filter: `${savedObjectType}.attributes.list_type: item`,
filter: adjustedFilter,
perPage: 0,
search: finalListId,
searchFields: ['list_id'],
Expand All @@ -84,7 +91,7 @@ export const getExceptionListSummary = async ({
(acc, item: ByOsAggBucketType) => ({
...acc,
[item.key]: item.doc_count,
total: acc.total + item.doc_count,
total: savedObject.total,
}),
{ linux: 0, macos: 0, total: 0, windows: 0 }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class EventFiltersHttpService implements EventFiltersService {
return deleteOne(this.http, id);
}

async getSummary(): Promise<ExceptionListSummarySchema> {
return getSummary(this.http);
async getSummary(filter?: string): Promise<ExceptionListSummarySchema> {
return getSummary({ http: this.http, filter });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,17 @@ export async function deleteOne(http: HttpStart, id: string): Promise<ExceptionL
});
}

export async function getSummary(http: HttpStart): Promise<ExceptionListSummarySchema> {
export async function getSummary({
http,
filter,
}: {
http: HttpStart;
filter?: string;
}): Promise<ExceptionListSummarySchema> {
await ensureEventFiltersListExists(http);
return http.get<ExceptionListSummarySchema>(`${EXCEPTION_LIST_URL}/summary`, {
query: {
filter,
list_id: ENDPOINT_EVENT_FILTERS_LIST_ID,
namespace_type: 'agnostic',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { combineReducers, createStore } from 'redux';
import type {
FoundExceptionListItemSchema,
ExceptionListItemSchema,
ExceptionListSummarySchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
import { Ecs } from '../../../../../common/ecs';
Expand All @@ -25,6 +26,7 @@ import {
} from '../../../../common/mock/endpoint/http_handler_mock_factory';
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getSummaryExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_summary_schema.mock';

export const createGlobalNoMiddlewareStore = () => {
return createStore(
Expand Down Expand Up @@ -107,6 +109,7 @@ export type EventFiltersListQueryHttpMockProviders = ResponseProvidersInterface<
eventFiltersGetOne: () => ExceptionListItemSchema;
eventFiltersCreateOne: () => ExceptionListItemSchema;
eventFiltersUpdateOne: () => ExceptionListItemSchema;
eventFiltersGetSummary: () => ExceptionListSummarySchema;
}>;

export const esResponseData = () => ({
Expand Down Expand Up @@ -255,4 +258,12 @@ export const eventFiltersListQueryHttpMock =
return getExceptionListItemSchemaMock();
},
},
{
id: 'eventFiltersGetSummary',
method: 'get',
path: `${EXCEPTION_LIST_URL}/summary`,
handler: (): ExceptionListSummarySchema => {
return getSummaryExceptionListSchemaMock();
},
},
]);
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export interface EventFiltersService {
getOne(id: string): Promise<ExceptionListItemSchema>;
updateOne(exception: Immutable<UpdateExceptionListItemSchema>): Promise<ExceptionListItemSchema>;
deleteOne(id: string): Promise<ExceptionListItemSchema>;
getSummary(): Promise<ExceptionListSummarySchema>;
getSummary(filter?: string): Promise<ExceptionListSummarySchema>;
}

export interface EventFiltersListPageData {
Expand Down
Loading