[Security Solution][Endpoint] Add event filters summary card to the fleet endpoint tab (#100668)

* Shows event filters card on fleet page

* Uses aggs instead of while loop to retrieve summary data

* Add request and response types in the lists package

* Fixes old import

* Removes old i18n keys

* Removes more old i18n keys

* Use consts for exception lists url and endpoint event filter list id

* Uses event filters service to retrieve summary data

* Fixes addressed pr comments such as changing the route without underscore, adding aggs type, validating response, and more

* Uses useMemo instead of useState to memoize object

* Add new e2e test for summart endpoint

* Handle api errors on event filters and trusted apps summary api calls

* Add api error message to the toast

* Fix wrong i18n key

* Change span tag by react fragment

* Uses styled components instead of modify compontent style directly and small improvements on test -> ts

* Adds curls script for summary route

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
David Sánchez 2021-05-28 18:07:54 +02:00 committed by GitHub
parent b4e8cfe0d7
commit cec62cb706
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 857 additions and 56 deletions

View file

@ -31,6 +31,7 @@ export * from './read_exception_list_item_schema';
export * from './read_exception_list_schema';
export * from './read_list_item_schema';
export * from './read_list_schema';
export * from './summary_exception_list_schema';
export * from './update_endpoint_list_item_schema';
export * from './update_exception_list_item_schema';
export * from './update_exception_list_item_validation';

View file

@ -0,0 +1,17 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ID, LIST_ID, NAMESPACE_TYPE } from '../../constants/index.mock';
import { SummaryExceptionListSchema } from '.';
export const getSummaryExceptionListSchemaMock = (): SummaryExceptionListSchema => ({
id: ID,
list_id: LIST_ID,
namespace_type: NAMESPACE_TYPE,
});

View file

@ -0,0 +1,126 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { left } from 'fp-ts/lib/Either';
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import { getSummaryExceptionListSchemaMock } from './index.mock';
import { SummaryExceptionListSchema, summaryExceptionListSchema } from '.';
describe('summary_exception_list_schema', () => {
test('it should validate a typical exception list request', () => {
const payload = getSummaryExceptionListSchemaMock();
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;
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 "list_id"', () => {
const payload = getSummaryExceptionListSchemaMock();
delete payload.list_id;
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 "namespace_type" but default to "single"', () => {
const payload = getSummaryExceptionListSchemaMock();
delete payload.namespace_type;
const decoded = summaryExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getSummaryExceptionListSchemaMock());
});
test('it should accept an undefined for "id", "list_id", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getSummaryExceptionListSchemaMock();
delete payload.id;
delete payload.namespace_type;
delete payload.list_id;
const output = getSummaryExceptionListSchemaMock();
delete output.id;
delete output.list_id;
const decoded = summaryExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should accept an undefined for "id", "list_id"', () => {
const payload = getSummaryExceptionListSchemaMock();
delete payload.id;
delete payload.list_id;
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", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getSummaryExceptionListSchemaMock();
delete payload.id;
delete payload.namespace_type;
const output = getSummaryExceptionListSchemaMock();
delete output.id;
const decoded = summaryExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should accept an undefined for "list_id", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getSummaryExceptionListSchemaMock();
delete payload.namespace_type;
delete payload.list_id;
const output = getSummaryExceptionListSchemaMock();
delete output.list_id;
const decoded = summaryExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should not allow an extra key to be sent in', () => {
const payload: SummaryExceptionListSchema & {
extraKey?: string;
} = getSummaryExceptionListSchemaMock();
payload.extraKey = 'some new value';
const decoded = summaryExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,33 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
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 { list_id } from '../../common/list_id';
import { namespace_type } from '../../common/namespace_type';
export const summaryExceptionListSchema = t.exact(
t.partial({
id,
list_id,
namespace_type, // defaults to 'single' if not set during decode
})
);
export type SummaryExceptionListSchema = t.OutputOf<typeof summaryExceptionListSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type SummaryExceptionListSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof summaryExceptionListSchema>>,
'namespace_type'
> & {
namespace_type: NamespaceType;
};

View file

@ -0,0 +1,16 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ExceptionListSummarySchema } from '.';
export const getListSummaryResponseMock = (): ExceptionListSummarySchema => ({
windows: 0,
linux: 1,
macos: 2,
total: 3,
});

View file

@ -0,0 +1,94 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { left } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import { getListSummaryResponseMock } from './index.mock';
import { ExceptionListSummarySchema, exceptionListSummarySchema } from '.';
describe('list_summary_schema', () => {
test('it should validate a typical list summary response', () => {
const payload = getListSummaryResponseMock();
const decoded = exceptionListSummarySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT accept an undefined for "windows"', () => {
const payload = getListSummaryResponseMock();
// @ts-expect-error
delete payload.windows;
const decoded = exceptionListSummarySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "windows"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "linux"', () => {
const payload = getListSummaryResponseMock();
// @ts-expect-error
delete payload.linux;
const decoded = exceptionListSummarySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "linux"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "macos"', () => {
const payload = getListSummaryResponseMock();
// @ts-expect-error
delete payload.macos;
const decoded = exceptionListSummarySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "macos"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "total"', () => {
const payload = getListSummaryResponseMock();
// @ts-expect-error
delete payload.total;
const decoded = exceptionListSummarySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "total"',
]);
expect(message.schema).toEqual({});
});
test('it should not allow an extra key to be sent in', () => {
const payload: ExceptionListSummarySchema & {
extraKey?: string;
} = getListSummaryResponseMock();
payload.extraKey = 'some new value';
const decoded = exceptionListSummarySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,21 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';
export const exceptionListSummarySchema = t.exact(
t.type({
windows: PositiveInteger,
linux: PositiveInteger,
macos: PositiveInteger,
total: PositiveInteger,
})
);
export type ExceptionListSummarySchema = t.TypeOf<typeof exceptionListSummarySchema>;

View file

@ -16,5 +16,6 @@ export * from './found_list_item_schema';
export * from './found_list_schema';
export * from './list_item_schema';
export * from './list_schema';
export * from './exception_list_summary_schema';
export * from './list_item_index_exist_schema';
export * from './search_list_item_schema';

View file

@ -36,6 +36,7 @@ export * from './read_list_index_route';
export * from './read_list_item_route';
export * from './read_list_route';
export * from './read_privileges_route';
export * from './summary_exception_list_route';
export * from './update_endpoint_list_item_route';
export * from './update_exception_list_item_route';
export * from './update_exception_list_route';

View file

@ -39,6 +39,7 @@ import {
readListItemRoute,
readListRoute,
readPrivilegesRoute,
summaryExceptionListRoute,
updateEndpointListItemRoute,
updateExceptionListItemRoute,
updateExceptionListRoute,
@ -95,4 +96,7 @@ export const initRoutes = (router: ListsPluginRouter, config: ConfigType): void
updateEndpointListItemRoute(router);
deleteEndpointListItemRoute(router);
findEndpointListItemRoute(router);
// exception list items summary
summaryExceptionListRoute(router);
};

View file

@ -0,0 +1,76 @@
/*
* 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 { validate } from '@kbn/securitysolution-io-ts-utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import {
SummaryExceptionListSchemaDecoded,
exceptionListSummarySchema,
summaryExceptionListSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
import type { ListsPluginRouter } from '../types';
import {
buildRouteValidation,
buildSiemResponse,
getErrorMessageExceptionList,
getExceptionListClient,
} from './utils';
export const summaryExceptionListRoute = (router: ListsPluginRouter): void => {
router.get(
{
options: {
tags: ['access:lists-summary'],
},
path: `${EXCEPTION_LIST_URL}/summary`,
validate: {
query: buildRouteValidation<
typeof summaryExceptionListSchema,
SummaryExceptionListSchemaDecoded
>(summaryExceptionListSchema),
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
const { id, list_id: listId, namespace_type: namespaceType } = request.query;
const exceptionLists = getExceptionListClient(context);
if (id != null || listId != null) {
const exceptionListSummary = await exceptionLists.getExceptionListSummary({
id,
listId,
namespaceType,
});
if (exceptionListSummary == null) {
return siemResponse.error({
body: getErrorMessageExceptionList({ id, listId }),
statusCode: 404,
});
} else {
const [validated, errors] = validate(exceptionListSummary, exceptionListSummarySchema);
if (errors != null) {
return response.ok({ body: exceptionListSummary });
} else {
return response.ok({ body: validated ?? {} });
}
}
} else {
return siemResponse.error({ body: 'id or list_id required', statusCode: 400 });
}
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -0,0 +1,25 @@
#!/bin/sh
#
# 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.
#
set -e
./check_env_variables.sh
LIST_ID=${1:-endpoint_list}
NAMESPACE_TYPE=${2-agnostic}
# First, post a exception list and two list items for the example to work
# ./post_exception_list.sh ./exception_lists/new/exception_list_agnostic.json
# ./post_exception_list_item.sh ./exception_lists/new/exception_list_item_agnostic.json
# Retrieve exception list stats by os
# Example: ./summary_exception_list.sh endpoint_list agnostic
curl -s -k \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
-X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/summary?list_id=${LIST_ID}&namespace_type=${NAMESPACE_TYPE}" | jq .

View file

@ -9,6 +9,7 @@ import { SavedObjectsClientContract } from 'kibana/server';
import type {
ExceptionListItemSchema,
ExceptionListSchema,
ExceptionListSummarySchema,
FoundExceptionListItemSchema,
FoundExceptionListSchema,
} from '@kbn/securitysolution-io-ts-list-types';
@ -31,11 +32,13 @@ import {
GetEndpointListItemOptions,
GetExceptionListItemOptions,
GetExceptionListOptions,
GetExceptionListSummaryOptions,
UpdateEndpointListItemOptions,
UpdateExceptionListItemOptions,
UpdateExceptionListOptions,
} from './exception_list_client_types';
import { getExceptionList } from './get_exception_list';
import { getExceptionListSummary } from './get_exception_list_summary';
import { createExceptionList } from './create_exception_list';
import { getExceptionListItem } from './get_exception_list_item';
import { createExceptionListItem } from './create_exception_list_item';
@ -72,6 +75,15 @@ export class ExceptionListClient {
return getExceptionList({ id, listId, namespaceType, savedObjectsClient });
};
public getExceptionListSummary = async ({
listId,
id,
namespaceType,
}: GetExceptionListSummaryOptions): Promise<ExceptionListSummarySchema | null> => {
const { savedObjectsClient } = this;
return getExceptionListSummary({ id, listId, namespaceType, savedObjectsClient });
};
public getExceptionListItem = async ({
itemId,
id,

View file

@ -56,6 +56,12 @@ export interface GetExceptionListOptions {
namespaceType: NamespaceType;
}
export interface GetExceptionListSummaryOptions {
listId: ListIdOrUndefined;
id: IdOrUndefined;
namespaceType: NamespaceType;
}
export interface CreateExceptionListOptions {
listId: ListId;
namespaceType: NamespaceType;

View file

@ -0,0 +1,93 @@
/*
* 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 type {
ExceptionListSummarySchema,
IdOrUndefined,
ListIdOrUndefined,
NamespaceType,
} from '@kbn/securitysolution-io-ts-list-types';
import { getSavedObjectType } from '@kbn/securitysolution-list-utils';
import {
SavedObjectsClientContract,
SavedObjectsErrorHelpers,
} from '../../../../../../src/core/server/';
import { ExceptionListSoSchema } from '../../schemas/saved_objects';
interface GetExceptionListSummaryOptions {
id: IdOrUndefined;
listId: ListIdOrUndefined;
savedObjectsClient: SavedObjectsClientContract;
namespaceType: NamespaceType;
}
interface ByOsAggBucketType {
key: string;
doc_count: number;
}
interface ByOsAggType {
by_os: {
buckets: ByOsAggBucketType[];
};
}
export const getExceptionListSummary = async ({
id,
listId,
savedObjectsClient,
namespaceType,
}: GetExceptionListSummaryOptions): Promise<ExceptionListSummarySchema | null> => {
const savedObjectType = getSavedObjectType({ namespaceType });
let finalListId: string = listId ?? '';
// If id and no listId, get the list by id to use the list_id for the find below
if (listId === null && id != null) {
try {
const savedObject = await savedObjectsClient.get<ExceptionListSoSchema>(savedObjectType, id);
finalListId = savedObject.attributes.list_id;
} catch (err) {
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
return null;
} else {
throw err;
}
}
}
const savedObject = await savedObjectsClient.find<ExceptionListSoSchema, ByOsAggType>({
aggs: {
by_os: {
terms: {
field: `${savedObjectType}.attributes.os_types`,
},
},
},
filter: `${savedObjectType}.attributes.list_type: item`,
perPage: 0,
search: finalListId,
searchFields: ['list_id'],
sortField: 'tie_breaker_id',
sortOrder: 'desc',
type: savedObjectType,
});
if (!savedObject.aggregations) {
return null;
}
const summary: ExceptionListSummarySchema = savedObject.aggregations.by_os.buckets.reduce(
(acc, item: ByOsAggBucketType) => ({
...acc,
[item.key]: item.doc_count,
total: acc.total + item.doc_count,
}),
{ linux: 0, macos: 0, total: 0, windows: 0 }
);
return summary;
};

View file

@ -15,5 +15,6 @@ export * from './find_exception_list_item';
export * from './find_exception_list_items';
export * from './get_exception_list';
export * from './get_exception_list_item';
export * from './get_exception_list_summary';
export * from './update_exception_list';
export * from './update_exception_list_item';

View file

@ -1095,3 +1095,13 @@ export interface GetAgentSummaryResponse {
versions_count: { [key: string]: number };
};
}
/**
* REST API response for retrieving exception summary
*/
export interface GetExceptionSummaryResponse {
total: number;
windows: number;
macos: number;
linux: number;
}

View file

@ -11,6 +11,7 @@ import type {
ExceptionListItemSchema,
CreateExceptionListItemSchema,
UpdateExceptionListItemSchema,
ExceptionListSummarySchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants';
@ -102,4 +103,16 @@ export class EventFiltersHttpService implements EventFiltersService {
},
});
}
async getSummary(): Promise<ExceptionListSummarySchema> {
return (await this.httpWrapper()).get<ExceptionListSummarySchema>(
`${EXCEPTION_LIST_URL}/summary`,
{
query: {
list_id: ENDPOINT_EVENT_FILTERS_LIST_ID,
namespace_type: 'agnostic',
},
}
);
}
}

View file

@ -29,6 +29,7 @@ const createEventFiltersServiceMock = (): jest.Mocked<EventFiltersService> => ({
getOne: jest.fn(),
updateOne: jest.fn(),
deleteOne: jest.fn(),
getSummary: jest.fn(),
});
const createStoreSetup = (eventFiltersService: EventFiltersService) => {

View file

@ -10,6 +10,7 @@ import type {
CreateExceptionListItemSchema,
ExceptionListItemSchema,
UpdateExceptionListItemSchema,
ExceptionListSummarySchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { AsyncResourceState } from '../../state/async_resource_state';
import { Immutable } from '../../../../common/endpoint/types';
@ -49,6 +50,7 @@ export interface EventFiltersService {
getOne(id: string): Promise<ExceptionListItemSchema>;
updateOne(exception: Immutable<UpdateExceptionListItemSchema>): Promise<ExceptionListItemSchema>;
deleteOne(id: string): Promise<ExceptionListItemSchema>;
getSummary(): Promise<ExceptionListSummarySchema>;
}
export interface EventFiltersListPageData {

View file

@ -6,56 +6,45 @@
*/
import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import React, { FC, memo, useEffect, useState } from 'react';
import { CoreStart } from 'kibana/public';
import React, { FC, memo } from 'react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public';
import { TrustedAppsHttpService } from '../../../../../trusted_apps/service';
import { GetTrustedAppsSummaryResponse } from '../../../../../../../../common/endpoint/types';
import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types';
const SUMMARY_KEYS: Readonly<Array<keyof GetTrustedAppsSummaryResponse>> = [
const SUMMARY_KEYS: Readonly<Array<keyof GetExceptionSummaryResponse>> = [
'windows',
'macos',
'linux',
'total',
];
const SUMMARY_LABELS: Readonly<{ [key in keyof GetTrustedAppsSummaryResponse]: string }> = {
const SUMMARY_LABELS: Readonly<{ [key in keyof GetExceptionSummaryResponse]: string }> = {
windows: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.windows',
'xpack.securitySolution.endpoint.fleetCustomExtension.exceptionItemsSummary.windows',
{ defaultMessage: 'Windows' }
),
linux: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.linux',
'xpack.securitySolution.endpoint.fleetCustomExtension.exceptionItemsSummary.linux',
{ defaultMessage: 'Linux' }
),
macos: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.macos',
'xpack.securitySolution.endpoint.fleetCustomExtension.exceptionItemsSummary.macos',
{ defaultMessage: 'Mac' }
),
total: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.total',
'xpack.securitySolution.endpoint.fleetCustomExtension.exceptionItemsSummary.total',
{ defaultMessage: 'Total' }
),
};
const CSS_BOLD: Readonly<React.CSSProperties> = { fontWeight: 'bold' };
export const TrustedAppItemsSummary = memo(() => {
const {
services: { http },
} = useKibana<CoreStart>();
const [stats, setStats] = useState<GetTrustedAppsSummaryResponse | undefined>();
const [trustedAppsApi] = useState(() => new TrustedAppsHttpService(http));
useEffect(() => {
trustedAppsApi.getTrustedAppsSummary().then((response) => {
setStats(response);
});
}, [trustedAppsApi]);
interface ExceptionItemsSummaryProps {
stats: GetExceptionSummaryResponse | undefined;
}
export const ExceptionItemsSummary = memo<ExceptionItemsSummaryProps>(({ stats }) => {
return (
<EuiFlexGroup responsive={false}>
<EuiFlexGroup alignItems="center" justifyContent="spaceAround">
{SUMMARY_KEYS.map((stat) => {
return (
<EuiFlexItem>
@ -73,18 +62,13 @@ export const TrustedAppItemsSummary = memo(() => {
);
});
TrustedAppItemsSummary.displayName = 'TrustedAppItemsSummary';
ExceptionItemsSummary.displayName = 'ExceptionItemsSummary';
const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color'] }> = memo(
({ children, value, color, ...commonProps }) => {
return (
<EuiText className="eui-displayInlineBlock" size="s">
<EuiFlexGroup
responsive={false}
justifyContent="center"
direction="row"
alignItems="center"
>
<EuiFlexGroup justifyContent="center" direction="row" alignItems="center">
<EuiFlexItem grow={false} style={color === 'primary' ? CSS_BOLD : undefined}>
{children}
</EuiFlexItem>

View file

@ -0,0 +1,116 @@
/*
* 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 React, { memo, useMemo, useState, useEffect } from 'react';
import { ApplicationStart, CoreStart } from 'kibana/public';
import { EuiPanel, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
PackageCustomExtensionComponentProps,
pagePathGetters,
} from '../../../../../../../../../fleet/public';
import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public';
import { getEventFiltersListPath } from '../../../../../../common/routing';
import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types';
import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common';
import { MANAGEMENT_APP_ID } from '../../../../../../common/constants';
import { useToasts } from '../../../../../../../common/lib/kibana';
import { LinkWithIcon } from './link_with_icon';
import { ExceptionItemsSummary } from './exception_items_summary';
import { EventFiltersHttpService } from '../../../../../event_filters/service';
import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components';
export const FleetEventFiltersCard = memo<PackageCustomExtensionComponentProps>(({ pkgkey }) => {
const {
services: {
application: { getUrlForApp },
http,
},
} = useKibana<CoreStart & { application: ApplicationStart }>();
const toasts = useToasts();
const [stats, setStats] = useState<GetExceptionSummaryResponse | undefined>();
const eventFiltersListUrlPath = getEventFiltersListPath();
const eventFiltersApi = useMemo(() => new EventFiltersHttpService(http), [http]);
useEffect(() => {
const fetchStats = async () => {
try {
const summary = await eventFiltersApi.getSummary();
setStats(summary);
} catch (error) {
toasts.addDanger(
i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummaryError',
{
defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"',
values: { error },
}
)
);
}
};
fetchStats();
}, [eventFiltersApi, toasts]);
const eventFiltersRouteState = useMemo(() => {
const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`;
return {
backButtonLabel: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel',
{ defaultMessage: 'Back to Endpoint Integration' }
),
onBackButtonNavigateTo: [
FLEET_PLUGIN_ID,
{
path: fleetPackageCustomUrlPath,
},
],
backButtonUrl: getUrlForApp(FLEET_PLUGIN_ID, {
path: fleetPackageCustomUrlPath,
}),
};
}, [getUrlForApp, pkgkey]);
return (
<EuiPanel paddingSize="l">
<StyledEuiFlexGridGroup alignItems="baseline" justifyContent="center">
<StyledEuiFlexGridItem gridArea="title" alignItems="flex-start">
<EuiText>
<h4>
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersLabel"
defaultMessage="Event Filters"
/>
</h4>
</EuiText>
</StyledEuiFlexGridItem>
<StyledEuiFlexGridItem gridArea="summary">
<ExceptionItemsSummary stats={stats} />
</StyledEuiFlexGridItem>
<StyledEuiFlexGridItem gridArea="link" alignItems="flex-end">
<>
<LinkWithIcon
appId={MANAGEMENT_APP_ID}
href={getUrlForApp(MANAGEMENT_APP_ID, { path: eventFiltersListUrlPath })}
appPath={eventFiltersListUrlPath}
appState={eventFiltersRouteState}
data-test-subj="linkToEventFilters"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetCustomExtension.manageEventFiltersLinkLabel"
defaultMessage="Manage event filters"
/>
</LinkWithIcon>
</>
</StyledEuiFlexGridItem>
</StyledEuiFlexGridGroup>
</EuiPanel>
);
});
FleetEventFiltersCard.displayName = 'FleetEventFiltersCard';

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import { ApplicationStart } from 'kibana/public';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import React, { memo, useMemo, useState, useEffect } from 'react';
import { ApplicationStart, CoreStart } from 'kibana/public';
import { EuiPanel, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@ -16,19 +16,48 @@ import {
} from '../../../../../../../../../fleet/public';
import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public';
import { getTrustedAppsListPath } from '../../../../../../common/routing';
import { TrustedAppsListPageRouteState } from '../../../../../../../../common/endpoint/types';
import {
TrustedAppsListPageRouteState,
GetExceptionSummaryResponse,
} from '../../../../../../../../common/endpoint/types';
import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common';
import { MANAGEMENT_APP_ID } from '../../../../../../common/constants';
import { useToasts } from '../../../../../../../common/lib/kibana';
import { LinkWithIcon } from './link_with_icon';
import { TrustedAppItemsSummary } from './trusted_app_items_summary';
import { ExceptionItemsSummary } from './exception_items_summary';
import { TrustedAppsHttpService } from '../../../../../trusted_apps/service';
import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components';
export const FleetTrustedAppsCard = memo<PackageCustomExtensionComponentProps>(({ pkgkey }) => {
const {
services: {
application: { getUrlForApp },
http,
},
} = useKibana<{ application: ApplicationStart }>();
} = useKibana<CoreStart & { application: ApplicationStart }>();
const toasts = useToasts();
const [stats, setStats] = useState<GetExceptionSummaryResponse | undefined>();
const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]);
useEffect(() => {
const fetchStats = async () => {
try {
const response = await trustedAppsApi.getTrustedAppsSummary();
setStats(response);
} catch (error) {
toasts.addDanger(
i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummaryError',
{
defaultMessage: 'There was an error trying to fetch trusted apps stats: "{error}"',
values: { error },
}
)
);
}
};
fetchStats();
}, [toasts, trustedAppsApi]);
const trustedAppsListUrlPath = getTrustedAppsListPath();
const trustedAppRouteState = useMemo<TrustedAppsListPageRouteState>(() => {
@ -52,8 +81,8 @@ export const FleetTrustedAppsCard = memo<PackageCustomExtensionComponentProps>((
return (
<EuiPanel paddingSize="l">
<EuiFlexGroup alignItems="baseline">
<EuiFlexItem>
<StyledEuiFlexGridGroup alignItems="baseline" justifyContent="center">
<StyledEuiFlexGridItem gridArea="title" alignItems="flex-start">
<EuiText>
<h4>
<FormattedMessage
@ -62,12 +91,12 @@ export const FleetTrustedAppsCard = memo<PackageCustomExtensionComponentProps>((
/>
</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TrustedAppItemsSummary />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<span>
</StyledEuiFlexGridItem>
<StyledEuiFlexGridItem gridArea="summary">
<ExceptionItemsSummary stats={stats} />
</StyledEuiFlexGridItem>
<StyledEuiFlexGridItem gridArea="link" alignItems="flex-end">
<>
<LinkWithIcon
appId={MANAGEMENT_APP_ID}
href={getUrlForApp(MANAGEMENT_APP_ID, { path: trustedAppsListUrlPath })}
@ -80,9 +109,9 @@ export const FleetTrustedAppsCard = memo<PackageCustomExtensionComponentProps>((
defaultMessage="Manage trusted applications"
/>
</LinkWithIcon>
</span>
</EuiFlexItem>
</EuiFlexGroup>
</>
</StyledEuiFlexGridItem>
</StyledEuiFlexGridGroup>
</EuiPanel>
);
});

View file

@ -0,0 +1,24 @@
/*
* 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 styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)`
display: grid;
grid-template-columns: 25% 45% 30%;
grid-template-areas: 'title summary link';
`;
export const StyledEuiFlexGridItem = styled(EuiFlexItem)<{
gridArea: string;
alignItems?: string;
}>`
grid-area: ${({ gridArea }) => gridArea};
align-items: ${({ alignItems }) => alignItems ?? 'center'};
margin: 0px;
padding: 12px;
`;

View file

@ -5,15 +5,19 @@
* 2.0.
*/
import { EuiSpacer } from '@elastic/eui';
import React, { memo } from 'react';
import { PackageCustomExtensionComponentProps } from '../../../../../../../../fleet/public';
import { FleetTrustedAppsCard } from './components/fleet_trusted_apps_card';
import { FleetEventFiltersCard } from './components/fleet_event_filters_card';
export const EndpointPackageCustomExtension = memo<PackageCustomExtensionComponentProps>(
(props) => {
return (
<div data-test-subj="fleetEndpointPackageCustomContent">
<FleetTrustedAppsCard {...props} />
<EuiSpacer />
<FleetEventFiltersCard {...props} />
</div>
);
}

View file

@ -20462,10 +20462,6 @@
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失敗} other {不明}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel": "エンドポイント統合に戻る",
"xpack.securitySolution.endpoint.fleetCustomExtension.manageTrustedAppLinkLabel": "信頼できるアプリケーションを管理",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.linux": "Linux",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.macos": "Mac",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.total": "合計",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.windows": "Windows",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsLabel": "信頼できるアプリケーション",
"xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "推奨のデフォルト値で統合が保存されます。後からこれを変更するには、エージェントポリシー内で Endpoint Security 統合を編集します。",
"xpack.securitySolution.endpoint.ingestToastMessage": "Fleetが設定中に失敗しました。",

View file

@ -20762,10 +20762,6 @@
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失败} other {未知}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel": "返回至终端集成",
"xpack.securitySolution.endpoint.fleetCustomExtension.manageTrustedAppLinkLabel": "管理受信任的应用程序",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.linux": "Linux",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.macos": "Mac",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.total": "合计",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.windows": "Windows",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsLabel": "受信任的应用程序",
"xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "我们将使用建议的默认值保存您的集成。稍后,您可以通过在代理策略中编辑 Endpoint Security 集成对其进行更改。",
"xpack.securitySolution.endpoint.ingestToastMessage": "Fleet 在设置期间失败。",

View file

@ -35,5 +35,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./find_exception_lists'));
loadTestFile(require.resolve('./find_exception_list_items'));
loadTestFile(require.resolve('./read_list_privileges'));
loadTestFile(require.resolve('./summary_exception_lists'));
});
};

View file

@ -0,0 +1,98 @@
/*
* 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 expect from '@kbn/expect';
import { ExceptionListSummarySchema } from '@kbn/securitysolution-io-ts-list-types';
import { EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants';
import { LIST_ID } from '../../../../plugins/lists/common/constants.mock';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock';
import { createListsIndex, deleteListsIndex, deleteAllExceptions } from '../../utils';
interface SummaryResponseType {
body: ExceptionListSummarySchema;
}
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const es = getService('es');
describe('summary_exception_lists', () => {
describe('summary exception lists', () => {
beforeEach(async () => {
await createListsIndex(supertest);
});
afterEach(async () => {
await deleteListsIndex(supertest);
await deleteAllExceptions(es);
});
it('should give a validation error if the list_id and the id are not supplied', async () => {
const { body } = await supertest
.get(`${EXCEPTION_LIST_URL}/summary`)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(400);
expect(body).to.eql({
message: 'id or list_id required',
status_code: 400,
});
});
it('should return init summary when there are no items created', async () => {
const { body }: SummaryResponseType = await supertest
.get(`${EXCEPTION_LIST_URL}/summary?list_id=${LIST_ID}`)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
const expected: ExceptionListSummarySchema = {
linux: 0,
macos: 0,
total: 0,
windows: 0,
};
expect(body).to.eql(expected);
});
it('should return right summary when there are items created', async () => {
await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const item = getCreateExceptionListItemMinimalSchemaMock();
for (const os of ['windows', 'linux', 'macos']) {
await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send({ ...item, os_types: [os], item_id: `${item.item_id}-${os}` })
.expect(200);
}
const { body }: SummaryResponseType = await supertest
.get(`${EXCEPTION_LIST_URL}/summary?list_id=${LIST_ID}`)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
const expected: ExceptionListSummarySchema = {
linux: 1,
macos: 1,
total: 3,
windows: 1,
};
expect(body).to.eql(expected);
});
});
});
};