From 85cf23b8922488139dc46e65b64f400bee660847 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 22 Oct 2024 13:17:31 +0200 Subject: [PATCH 01/12] [Inventory] Fix Inventory storybook (#197174) part of [#196142](https://github.com/elastic/kibana/issues/196142) ## Summary This PR fixes the Inventory storybook image ### How to test - Run `yarn storybook inventory` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../.storybook/get_mock_inventory_context.tsx | 27 +- .../inventory/.storybook/jest_setup.js | 11 - .../inventory/.storybook/{main.js => main.ts} | 4 +- .../.storybook/{preview.js => preview.tsx} | 6 +- .../.storybook/storybook_decorator.tsx | 13 +- .../inventory/jest.config.js | 3 - .../entities_grid/entities_grid.stories.tsx | 58 +- .../entities_grid/mock/entities_mock.ts | 3046 +---------------- .../inventory/tsconfig.json | 3 +- 9 files changed, 126 insertions(+), 3045 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js rename x-pack/plugins/observability_solution/inventory/.storybook/{main.js => main.ts} (75%) rename x-pack/plugins/observability_solution/inventory/.storybook/{preview.js => preview.tsx} (63%) diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx index 52ec669a9a75c..9c2ea13cf753e 100644 --- a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx +++ b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx @@ -12,8 +12,10 @@ import type { EntityManagerPublicPluginStart } from '@kbn/entityManager-plugin/p import type { InferencePublicStart } from '@kbn/inference-plugin/public'; import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { LocatorPublic, SharePluginStart } from '@kbn/share-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import type { HttpStart } from '@kbn/core-http-browser'; +import { action } from '@storybook/addon-actions'; import type { InventoryKibanaContext } from '../public/hooks/use_kibana'; import type { ITelemetryClient } from '../public/services/telemetry/types'; @@ -25,7 +27,21 @@ export function getMockInventoryContext(): InventoryKibanaContext { entityManager: {} as unknown as EntityManagerPublicPluginStart, observabilityShared: {} as unknown as ObservabilitySharedPluginStart, inference: {} as unknown as InferencePublicStart, - share: {} as unknown as SharePluginStart, + share: { + url: { + locators: { + get: (_id: string) => + ({ + navigate: async () => { + return Promise.resolve(); + }, + getRedirectUrl: (args: any) => { + action('share.url.locators.getRedirectUrl')(args); + }, + } as unknown as LocatorPublic), + }, + }, + } as unknown as SharePluginStart, telemetry: {} as unknown as ITelemetryClient, unifiedSearch: {} as unknown as UnifiedSearchPublicPluginStart, dataViews: {} as unknown as DataViewsPublicPluginStart, @@ -34,6 +50,13 @@ export function getMockInventoryContext(): InventoryKibanaContext { fetch: jest.fn(), stream: jest.fn(), }, + http: { + basePath: { + prepend: (_path: string) => { + return ''; + }, + }, + } as unknown as HttpStart, spaces: {} as unknown as SpacesPluginStart, kibanaEnvironment: { isCloudEnv: false, diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js b/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js deleted file mode 100644 index 32071b8aa3f62..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * 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 { setGlobalConfig } from '@storybook/testing-react'; -import * as globalStorybookConfig from './preview'; - -setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/main.js b/x-pack/plugins/observability_solution/inventory/.storybook/main.ts similarity index 75% rename from x-pack/plugins/observability_solution/inventory/.storybook/main.js rename to x-pack/plugins/observability_solution/inventory/.storybook/main.ts index 86b48c32f103e..bf63e08d64c32 100644 --- a/x-pack/plugins/observability_solution/inventory/.storybook/main.js +++ b/x-pack/plugins/observability_solution/inventory/.storybook/main.ts @@ -5,4 +5,6 @@ * 2.0. */ -module.exports = require('@kbn/storybook').defaultConfig; +import { defaultConfig } from '@kbn/storybook'; + +module.exports = defaultConfig; diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/preview.js b/x-pack/plugins/observability_solution/inventory/.storybook/preview.tsx similarity index 63% rename from x-pack/plugins/observability_solution/inventory/.storybook/preview.js rename to x-pack/plugins/observability_solution/inventory/.storybook/preview.tsx index c8155e9c3d92c..9bcd37d60628a 100644 --- a/x-pack/plugins/observability_solution/inventory/.storybook/preview.js +++ b/x-pack/plugins/observability_solution/inventory/.storybook/preview.tsx @@ -5,9 +5,11 @@ * 2.0. */ -import { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common'; +import { addDecorator } from '@storybook/react'; import * as jest from 'jest-mock'; +import { KibanaReactStorybookDecorator } from './storybook_decorator'; +// @ts-ignore window.jest = jest; -export const decorators = [EuiThemeProviderDecorator]; +addDecorator(KibanaReactStorybookDecorator); diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/storybook_decorator.tsx b/x-pack/plugins/observability_solution/inventory/.storybook/storybook_decorator.tsx index 20e507e54b5d5..8c98289608d92 100644 --- a/x-pack/plugins/observability_solution/inventory/.storybook/storybook_decorator.tsx +++ b/x-pack/plugins/observability_solution/inventory/.storybook/storybook_decorator.tsx @@ -4,15 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { ComponentType, useMemo } from 'react'; +import React, { useMemo } from 'react'; +import { DecoratorFn } from '@storybook/react'; import { InventoryContextProvider } from '../public/context/inventory_context_provider'; import { getMockInventoryContext } from './get_mock_inventory_context'; -export function KibanaReactStorybookDecorator(Story: ComponentType) { +export const KibanaReactStorybookDecorator: DecoratorFn = (story) => { const context = useMemo(() => getMockInventoryContext(), []); - return ( - - - - ); -} + return {story()}; +}; diff --git a/x-pack/plugins/observability_solution/inventory/jest.config.js b/x-pack/plugins/observability_solution/inventory/jest.config.js index 4e4450567243c..4fd85ffa49368 100644 --- a/x-pack/plugins/observability_solution/inventory/jest.config.js +++ b/x-pack/plugins/observability_solution/inventory/jest.config.js @@ -13,9 +13,6 @@ module.exports = { '/x-pack/plugins/observability_solution/inventory/common', '/x-pack/plugins/observability_solution/inventory/server', ], - setupFiles: [ - '/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js', - ], collectCoverage: true, collectCoverageFrom: [ '/x-pack/plugins/observability_solution/inventory/{public,common,server}/**/*.{js,ts,tsx}', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx index 1f4d3f1f34b40..a89781ad2742a 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx @@ -5,51 +5,69 @@ * 2.0. */ -import { EuiDataGridSorting, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { EuiButton, EuiDataGridSorting, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Meta, Story } from '@storybook/react'; import { orderBy } from 'lodash'; import React, { useMemo, useState } from 'react'; import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { useArgs } from '@storybook/addons'; import { EntitiesGrid } from '.'; import { EntityType } from '../../../common/entities'; import { entitiesMock } from './mock/entities_mock'; -const stories: Meta<{}> = { +interface EntityGridStoriesArgs { + entityType?: EntityType; +} + +const entityTypeOptions: EntityType[] = ['host', 'container', 'service']; + +const stories: Meta = { title: 'app/inventory/entities_grid', component: EntitiesGrid, + argTypes: { + entityType: { + options: entityTypeOptions, + name: 'Entity type', + control: { + type: 'select', + }, + }, + }, + args: { entityType: undefined }, }; -export default stories; -export const Example: Story<{}> = () => { +export const Grid: Story = (args) => { const [pageIndex, setPageIndex] = useState(0); + const [{ entityType }, updateArgs] = useArgs(); const [sort, setSort] = useState({ id: ENTITY_LAST_SEEN, direction: 'desc', }); - const [selectedEntityType, setSelectedEntityType] = useState(); const filteredAndSortedItems = useMemo( () => orderBy( - selectedEntityType - ? entitiesMock.filter((mock) => mock[ENTITY_TYPE] === selectedEntityType) - : entitiesMock, + entityType ? entitiesMock.filter((mock) => mock[ENTITY_TYPE] === entityType) : entitiesMock, sort.id, sort.direction ), - [selectedEntityType, sort.direction, sort.id] + [entityType, sort.direction, sort.id] ); return ( - {`Entity filter: ${selectedEntityType || 'N/A'}`} - setSelectedEntityType(undefined)} - > - Clear filter - + + {`Entity filter: ${entityType || 'N/A'}`} + + updateArgs({ entityType: undefined })} + > + Clear filter + + + = () => { onChangePage={setPageIndex} onChangeSort={setSort} pageIndex={pageIndex} - onFilterByType={setSelectedEntityType} + onFilterByType={(selectedEntityType) => updateArgs({ entityType: selectedEntityType })} /> ); }; -export const EmptyGridExample: Story<{}> = () => { +export const EmptyGrid: Story = (args) => { const [pageIndex, setPageIndex] = useState(0); const [sort, setSort] = useState({ id: ENTITY_LAST_SEEN, @@ -87,3 +105,5 @@ export const EmptyGridExample: Story<{}> = () => { /> ); }; + +export default stories; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts index bf72d5d7832cf..8a34a9f68c7b6 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts @@ -5,3016 +5,66 @@ * 2.0. */ -import { APIReturnType } from '../../../api'; +import { faker } from '@faker-js/faker'; +import { + ENTITY_DISPLAY_NAME, + ENTITY_TYPE, + ENTITY_ID, + ENTITY_LAST_SEEN, +} from '@kbn/observability-shared-plugin/common'; +import { Entity, EntityType } from '../../../../common/entities'; -type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; +const idGenerator = () => { + let id = 0; + return () => (++id).toString(); +}; -export const entitiesMock = [ +const generateId = idGenerator(); + +function generateRandomTimestamp() { + const end = new Date(); + const start = new Date(end); + + start.setHours(start.getHours() - 24); + const randomDate = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); + + return randomDate.toISOString(); +} + +const getEntity = (entityType: EntityType) => ({ + [ENTITY_LAST_SEEN]: generateRandomTimestamp(), + [ENTITY_TYPE]: entityType, + [ENTITY_DISPLAY_NAME]: faker.person.fullName(), + [ENTITY_ID]: generateId(), +}); + +const alertsMock = [ { - 'entity.lastSeenTimestamp': '2023-08-20T10:50:06.384Z', - 'entity.type': 'host', - 'entity.displayName': 'Spider-Man', - 'entity.id': '0', + ...getEntity('host'), alertsCount: 3, }, { - 'entity.lastSeenTimestamp': '2024-06-16T21:48:16.259Z', - 'entity.type': 'service', - 'entity.displayName': 'Iron Man', - 'entity.id': '1', + ...getEntity('service'), alertsCount: 3, }, { - 'entity.lastSeenTimestamp': '2024-04-28T03:31:57.528Z', - 'entity.type': 'host', - 'entity.displayName': 'Captain America', - 'entity.id': '2', + ...getEntity('host'), alertsCount: 10, }, { - 'entity.lastSeenTimestamp': '2024-05-14T11:32:04.275Z', - 'entity.type': 'host', - 'entity.displayName': 'Hulk', - 'entity.id': '3', + ...getEntity('host'), alertsCount: 1, }, - { - 'entity.lastSeenTimestamp': '2023-12-05T13:33:54.028Z', - 'entity.type': 'container', - 'entity.displayName': 'Thor', - 'entity.id': '4', - }, - { - 'entity.lastSeenTimestamp': '2023-11-27T06:18:52.650Z', - 'entity.type': 'service', - 'entity.displayName': 'Black Widow', - 'entity.id': '5', - }, - { - 'entity.lastSeenTimestamp': '2023-06-23T21:20:41.460Z', - 'entity.type': 'container', - 'entity.displayName': 'Batman', - 'entity.id': '6', - }, - { - 'entity.lastSeenTimestamp': '2023-12-08T03:23:09.317Z', - 'entity.type': 'service', - 'entity.displayName': 'Superman', - 'entity.id': '7', - }, - { - 'entity.lastSeenTimestamp': '2024-08-07T19:06:52.169Z', - 'entity.type': 'service', - 'entity.displayName': 'Wonder Woman', - 'entity.id': '8', - }, - { - 'entity.lastSeenTimestamp': '2024-08-15T01:15:23.589Z', - 'entity.type': 'container', - 'entity.displayName': 'Aquaman', - 'entity.id': '9', - }, - { - 'entity.lastSeenTimestamp': '2024-05-18T09:44:35.799Z', - 'entity.type': 'service', - 'entity.displayName': 'Flash', - 'entity.id': '10', - }, - { - 'entity.lastSeenTimestamp': '2023-12-20T19:12:29.251Z', - 'entity.type': 'container', - 'entity.displayName': 'Cyborg', - 'entity.id': '11', - }, - { - 'entity.lastSeenTimestamp': '2024-04-04T02:52:28.431Z', - 'entity.type': 'container', - 'entity.displayName': 'Wolverine', - 'entity.id': '12', - }, - { - 'entity.lastSeenTimestamp': '2023-07-14T05:13:12.906Z', - 'entity.type': 'host', - 'entity.displayName': 'Deadpool', - 'entity.id': '13', - }, - { - 'entity.lastSeenTimestamp': '2023-07-21T07:30:55.389Z', - 'entity.type': 'service', - 'entity.displayName': 'Green Lantern', - 'entity.id': '14', - }, - { - 'entity.lastSeenTimestamp': '2024-06-16T09:30:32.331Z', - 'entity.type': 'service', - 'entity.displayName': 'Doctor Strange', - 'entity.id': '15', - }, - { - 'entity.lastSeenTimestamp': '2023-08-24T08:05:46.687Z', - 'entity.type': 'container', - 'entity.displayName': 'Ant-Man', - 'entity.id': '16', - }, - { - 'entity.lastSeenTimestamp': '2024-03-23T09:37:36.874Z', - 'entity.type': 'service', - 'entity.displayName': 'Scarlet Witch', - 'entity.id': '17', - }, - { - 'entity.lastSeenTimestamp': '2023-05-12T02:34:46.188Z', - 'entity.type': 'host', - 'entity.displayName': 'Black Panther', - 'entity.id': '18', - }, - { - 'entity.lastSeenTimestamp': '2023-01-05T07:16:17.213Z', - 'entity.type': 'container', - 'entity.displayName': 'Captain Marvel', - 'entity.id': '19', - }, - { - 'entity.lastSeenTimestamp': '2024-05-28T04:08:43.047Z', - 'entity.type': 'host', - 'entity.displayName': 'Hawkeye', - 'entity.id': '20', - }, - { - 'entity.lastSeenTimestamp': '2024-04-23T02:01:01.149Z', - 'entity.type': 'service', - 'entity.displayName': 'Vision', - 'entity.id': '21', - }, - { - 'entity.lastSeenTimestamp': '2023-04-08T10:40:14.658Z', - 'entity.type': 'host', - 'entity.displayName': 'Shazam', - 'entity.id': '22', - }, - { - 'entity.lastSeenTimestamp': '2024-01-11T09:03:11.465Z', - 'entity.type': 'service', - 'entity.displayName': 'Nightwing', - 'entity.id': '23', - }, - { - 'entity.lastSeenTimestamp': '2024-04-27T22:35:18.822Z', - 'entity.type': 'container', - 'entity.displayName': 'Robin', - 'entity.id': '24', - }, - { - 'entity.lastSeenTimestamp': '2023-03-09T22:05:08.071Z', - 'entity.type': 'container', - 'entity.displayName': 'Starfire', - 'entity.id': '25', - }, - { - 'entity.lastSeenTimestamp': '2024-08-09T13:20:31.960Z', - 'entity.type': 'service', - 'entity.displayName': 'Beast Boy', - 'entity.id': '26', - }, - { - 'entity.lastSeenTimestamp': '2024-07-12T01:44:33.204Z', - 'entity.type': 'service', - 'entity.displayName': 'Raven', - 'entity.id': '27', - }, - { - 'entity.lastSeenTimestamp': '2023-01-31T00:08:53.817Z', - 'entity.type': 'service', - 'entity.displayName': 'Daredevil', - 'entity.id': '28', - }, - { - 'entity.lastSeenTimestamp': '2024-03-26T08:37:11.019Z', - 'entity.type': 'container', - 'entity.displayName': 'Luke Cage', - 'entity.id': '29', - }, - { - 'entity.lastSeenTimestamp': '2023-05-17T08:49:09.112Z', - 'entity.type': 'service', - 'entity.displayName': 'Jessica Jones', - 'entity.id': '30', - }, - { - 'entity.lastSeenTimestamp': '2024-06-15T20:05:12.395Z', - 'entity.type': 'service', - 'entity.displayName': 'Punisher', - 'entity.id': '31', - }, - { - 'entity.lastSeenTimestamp': '2024-07-30T06:53:16.477Z', - 'entity.type': 'service', - 'entity.displayName': 'Groot', - 'entity.id': '32', - }, - { - 'entity.lastSeenTimestamp': '2024-06-01T13:22:53.973Z', - 'entity.type': 'host', - 'entity.displayName': 'Rocket Raccoon', - 'entity.id': '33', - }, - { - 'entity.lastSeenTimestamp': '2024-09-12T17:44:12.492Z', - 'entity.type': 'container', - 'entity.displayName': 'Gamora', - 'entity.id': '34', - }, - { - 'entity.lastSeenTimestamp': '2024-03-28T13:44:52.732Z', - 'entity.type': 'service', - 'entity.displayName': 'Drax', - 'entity.id': '35', - }, - { - 'entity.lastSeenTimestamp': '2023-09-19T01:20:23.901Z', - 'entity.type': 'container', - 'entity.displayName': 'Mantis', - 'entity.id': '36', - }, - { - 'entity.lastSeenTimestamp': '2023-01-17T07:04:52.387Z', - 'entity.type': 'service', - 'entity.displayName': 'Winter Soldier', - 'entity.id': '37', - }, - { - 'entity.lastSeenTimestamp': '2023-10-07T15:08:39.776Z', - 'entity.type': 'host', - 'entity.displayName': 'Falcon', - 'entity.id': '38', - }, - { - 'entity.lastSeenTimestamp': '2024-05-01T17:45:43.595Z', - 'entity.type': 'host', - 'entity.displayName': 'Silver Surfer', - 'entity.id': '39', - }, - { - 'entity.lastSeenTimestamp': '2023-01-12T19:33:15.526Z', - 'entity.type': 'host', - 'entity.displayName': 'Moon Knight', - 'entity.id': '40', - }, - { - 'entity.lastSeenTimestamp': '2023-03-28T23:24:20.896Z', - 'entity.type': 'container', - 'entity.displayName': 'She-Hulk', - 'entity.id': '41', - }, - { - 'entity.lastSeenTimestamp': '2023-03-15T09:52:58.134Z', - 'entity.type': 'container', - 'entity.displayName': 'Blade', - 'entity.id': '42', - }, - { - 'entity.lastSeenTimestamp': '2023-04-18T07:38:32.158Z', - 'entity.type': 'container', - 'entity.displayName': 'Ghost Rider', - 'entity.id': '43', - }, - { - 'entity.lastSeenTimestamp': '2024-03-16T16:36:47.704Z', - 'entity.type': 'host', - 'entity.displayName': 'Cyclops', - 'entity.id': '44', - }, - { - 'entity.lastSeenTimestamp': '2023-06-11T13:40:02.951Z', - 'entity.type': 'service', - 'entity.displayName': 'Jean Grey', - 'entity.id': '45', - }, - { - 'entity.lastSeenTimestamp': '2024-09-11T23:54:53.129Z', - 'entity.type': 'container', - 'entity.displayName': 'Storm', - 'entity.id': '46', - }, - { - 'entity.lastSeenTimestamp': '2024-03-31T15:26:58.694Z', - 'entity.type': 'host', - 'entity.displayName': 'Iceman', - 'entity.id': '47', - }, - { - 'entity.lastSeenTimestamp': '2023-01-15T05:36:56.655Z', - 'entity.type': 'host', - 'entity.displayName': 'Colossus', - 'entity.id': '48', - }, - { - 'entity.lastSeenTimestamp': '2024-06-01T22:59:08.883Z', - 'entity.type': 'service', - 'entity.displayName': 'Kitty Pryde', - 'entity.id': '49', - }, - { - 'entity.lastSeenTimestamp': '2024-04-16T21:38:10.398Z', - 'entity.type': 'container', - 'entity.displayName': 'Psylocke', - 'entity.id': '50', - }, - { - 'entity.lastSeenTimestamp': '2023-02-13T07:41:37.539Z', - 'entity.type': 'container', - 'entity.displayName': 'Rogue', - 'entity.id': '51', - }, - { - 'entity.lastSeenTimestamp': '2023-12-11T14:40:29.422Z', - 'entity.type': 'service', - 'entity.displayName': 'Professor X', - 'entity.id': '52', - }, - { - 'entity.lastSeenTimestamp': '2023-03-06T09:50:33.183Z', - 'entity.type': 'host', - 'entity.displayName': 'Magneto', - 'entity.id': '53', - }, - { - 'entity.lastSeenTimestamp': '2024-06-30T14:52:19.840Z', - 'entity.type': 'host', - 'entity.displayName': 'Quicksilver', - 'entity.id': '54', - }, - { - 'entity.lastSeenTimestamp': '2023-08-16T01:03:06.855Z', - 'entity.type': 'container', - 'entity.displayName': 'Scarlet Witch', - 'entity.id': '55', - }, - { - 'entity.lastSeenTimestamp': '2023-12-19T23:23:08.821Z', - 'entity.type': 'host', - 'entity.displayName': 'Black Bolt', - 'entity.id': '56', - }, - { - 'entity.lastSeenTimestamp': '2024-01-04T06:04:23.837Z', - 'entity.type': 'service', - 'entity.displayName': 'Medusa', - 'entity.id': '57', - }, - { - 'entity.lastSeenTimestamp': '2024-01-02T11:03:36.265Z', - 'entity.type': 'container', - 'entity.displayName': 'Crystal', - 'entity.id': '58', - }, - { - 'entity.lastSeenTimestamp': '2023-01-14T04:12:51.710Z', - 'entity.type': 'service', - 'entity.displayName': 'Karnak', - 'entity.id': '59', - }, - { - 'entity.lastSeenTimestamp': '2023-09-16T15:31:25.215Z', - 'entity.type': 'container', - 'entity.displayName': 'Gorgon', - 'entity.id': '60', - }, - { - 'entity.lastSeenTimestamp': '2023-03-19T23:21:32.571Z', - 'entity.type': 'container', - 'entity.displayName': 'Triton', - 'entity.id': '61', - }, - { - 'entity.lastSeenTimestamp': '2024-02-08T21:57:35.600Z', - 'entity.type': 'host', - 'entity.displayName': 'Lockjaw', - 'entity.id': '62', - }, - { - 'entity.lastSeenTimestamp': '2024-02-26T03:18:43.161Z', - 'entity.type': 'container', - 'entity.displayName': 'Namor', - 'entity.id': '63', - }, - { - 'entity.lastSeenTimestamp': '2024-03-13T13:39:54.430Z', - 'entity.type': 'host', - 'entity.displayName': 'Hercules', - 'entity.id': '64', - }, - { - 'entity.lastSeenTimestamp': '2024-06-15T15:57:15.557Z', - 'entity.type': 'host', - 'entity.displayName': 'Valkyrie', - 'entity.id': '65', - }, - { - 'entity.lastSeenTimestamp': '2023-09-14T15:29:09.268Z', - 'entity.type': 'host', - 'entity.displayName': 'Sif', - 'entity.id': '66', - }, - { - 'entity.lastSeenTimestamp': '2023-06-06T11:32:45.998Z', - 'entity.type': 'service', - 'entity.displayName': 'Heimdall', - 'entity.id': '67', - }, - { - 'entity.lastSeenTimestamp': '2023-06-23T20:19:29.918Z', - 'entity.type': 'container', - 'entity.displayName': 'Loki', - 'entity.id': '68', - }, - { - 'entity.lastSeenTimestamp': '2024-02-15T19:08:56.703Z', - 'entity.type': 'service', - 'entity.displayName': 'Odin', - 'entity.id': '69', - }, - { - 'entity.lastSeenTimestamp': '2024-05-05T21:13:36.761Z', - 'entity.type': 'host', - 'entity.displayName': 'Enchantress', - 'entity.id': '70', - }, - { - 'entity.lastSeenTimestamp': '2023-07-29T20:51:41.023Z', - 'entity.type': 'container', - 'entity.displayName': 'Executioner', - 'entity.id': '71', - }, - { - 'entity.lastSeenTimestamp': '2023-08-06T17:17:53.101Z', - 'entity.type': 'container', - 'entity.displayName': 'Balder', - 'entity.id': '72', - }, - { - 'entity.lastSeenTimestamp': '2023-07-03T05:18:36.705Z', - 'entity.type': 'container', - 'entity.displayName': 'Beta Ray Bill', - 'entity.id': '73', - }, - { - 'entity.lastSeenTimestamp': '2023-05-26T14:32:39.569Z', - 'entity.type': 'container', - 'entity.displayName': 'Adam Warlock', - 'entity.id': '74', - }, - { - 'entity.lastSeenTimestamp': '2023-04-22T20:19:48.018Z', - 'entity.type': 'host', - 'entity.displayName': 'Ego the Living Planet', - 'entity.id': '75', - }, - { - 'entity.lastSeenTimestamp': '2024-09-01T05:03:37.465Z', - 'entity.type': 'container', - 'entity.displayName': 'Ronan the Accuser', - 'entity.id': '76', - }, - { - 'entity.lastSeenTimestamp': '2024-03-30T18:51:01.608Z', - 'entity.type': 'service', - 'entity.displayName': 'Nebula', - 'entity.id': '77', - }, - { - 'entity.lastSeenTimestamp': '2023-08-03T00:46:22.909Z', - 'entity.type': 'container', - 'entity.displayName': 'Yondu', - 'entity.id': '78', - }, - { - 'entity.lastSeenTimestamp': '2024-03-22T19:27:42.105Z', - 'entity.type': 'container', - 'entity.displayName': 'Star-Lord', - 'entity.id': '79', - }, - { - 'entity.lastSeenTimestamp': '2023-03-01T12:52:43.009Z', - 'entity.type': 'service', - 'entity.displayName': 'Elektra', - 'entity.id': '80', - }, - { - 'entity.lastSeenTimestamp': '2024-03-01T03:35:49.365Z', - 'entity.type': 'container', - 'entity.displayName': 'Bullseye', - 'entity.id': '81', - }, - { - 'entity.lastSeenTimestamp': '2023-04-23T03:29:05.951Z', - 'entity.type': 'service', - 'entity.displayName': 'Kingpin', - 'entity.id': '82', - }, - { - 'entity.lastSeenTimestamp': '2023-08-19T14:56:49.093Z', - 'entity.type': 'container', - 'entity.displayName': 'Iron Fist', - 'entity.id': '83', - }, - { - 'entity.lastSeenTimestamp': '2023-04-17T09:03:32.311Z', - 'entity.type': 'service', - 'entity.displayName': 'Misty Knight', - 'entity.id': '84', - }, - { - 'entity.lastSeenTimestamp': '2024-06-23T06:42:12.471Z', - 'entity.type': 'service', - 'entity.displayName': 'Colleen Wing', - 'entity.id': '85', - }, - { - 'entity.lastSeenTimestamp': '2023-10-20T10:59:37.573Z', - 'entity.type': 'host', - 'entity.displayName': 'Shang-Chi', - 'entity.id': '86', - }, - { - 'entity.lastSeenTimestamp': '2024-01-18T10:07:55.134Z', - 'entity.type': 'host', - 'entity.displayName': 'Black Cat', - 'entity.id': '87', - }, - { - 'entity.lastSeenTimestamp': '2024-09-04T14:02:31.795Z', - 'entity.type': 'container', - 'entity.displayName': 'Silver Sable', - 'entity.id': '88', - }, - { - 'entity.lastSeenTimestamp': '2023-09-21T16:08:59.195Z', - 'entity.type': 'container', - 'entity.displayName': 'Spider-Woman', - 'entity.id': '89', - }, - { - 'entity.lastSeenTimestamp': '2024-07-12T00:22:45.521Z', - 'entity.type': 'container', - 'entity.displayName': 'Dr. Nick', - 'entity.id': '90', - }, - { - 'entity.lastSeenTimestamp': '2023-06-27T20:43:47.331Z', - 'entity.type': 'container', - 'entity.displayName': 'Miles Morales', - 'entity.id': '91', - }, - { - 'entity.lastSeenTimestamp': '2023-11-15T05:35:28.421Z', - 'entity.type': 'host', - 'entity.displayName': 'Spider-Girl', - 'entity.id': '92', - }, - { - 'entity.lastSeenTimestamp': '2023-07-17T13:28:37.477Z', - 'entity.type': 'container', - 'entity.displayName': 'Nova', - 'entity.id': '93', - }, - { - 'entity.lastSeenTimestamp': '2024-05-13T09:58:21.185Z', - 'entity.type': 'container', - 'entity.displayName': 'Quasar', - 'entity.id': '94', - }, - { - 'entity.lastSeenTimestamp': '2023-09-22T18:29:20.589Z', - 'entity.type': 'container', - 'entity.displayName': 'Mar-Vell', - 'entity.id': '95', - }, - { - 'entity.lastSeenTimestamp': '2024-04-29T21:33:36.318Z', - 'entity.type': 'container', - 'entity.displayName': 'Monica Rambeau', - 'entity.id': '96', - }, - { - 'entity.lastSeenTimestamp': '2024-01-10T17:12:02.785Z', - 'entity.type': 'host', - 'entity.displayName': 'Photon', - 'entity.id': '97', - }, - { - 'entity.lastSeenTimestamp': '2024-08-03T04:59:46.730Z', - 'entity.type': 'container', - 'entity.displayName': 'Blue Marvel', - 'entity.id': '98', - }, - { - 'entity.lastSeenTimestamp': '2023-04-22T05:48:54.665Z', - 'entity.type': 'host', - 'entity.displayName': 'Sentry', - 'entity.id': '99', - }, - { - 'entity.lastSeenTimestamp': '2024-05-08T05:53:56.652Z', - 'entity.type': 'host', - 'entity.displayName': 'Hyperion', - 'entity.id': '100', - }, - { - 'entity.lastSeenTimestamp': '2024-08-21T08:45:38.667Z', - 'entity.type': 'service', - 'entity.displayName': 'Nighthawk', - 'entity.id': '101', - }, - { - 'entity.lastSeenTimestamp': '2024-08-15T14:03:39.798Z', - 'entity.type': 'host', - 'entity.displayName': 'Power Princess', - 'entity.id': '102', - }, - { - 'entity.lastSeenTimestamp': '2024-05-01T13:28:15.225Z', - 'entity.type': 'service', - 'entity.displayName': 'Doctor Spectrum', - 'entity.id': '103', - }, - { - 'entity.lastSeenTimestamp': '2023-01-21T21:03:45.309Z', - 'entity.type': 'container', - 'entity.displayName': 'Speed Demon', - 'entity.id': '104', - }, - { - 'entity.lastSeenTimestamp': '2023-03-29T06:15:14.140Z', - 'entity.type': 'container', - 'entity.displayName': 'Whizzer', - 'entity.id': '105', - }, - { - 'entity.lastSeenTimestamp': '2024-01-25T09:23:14.336Z', - 'entity.type': 'container', - 'entity.displayName': 'Scarlet Spider', - 'entity.id': '106', - }, - { - 'entity.lastSeenTimestamp': '2023-08-07T16:59:31.739Z', - 'entity.type': 'host', - 'entity.displayName': 'Kaine', - 'entity.id': '107', - }, - { - 'entity.lastSeenTimestamp': '2024-03-11T20:29:44.832Z', - 'entity.type': 'host', - 'entity.displayName': 'Ben Reilly', - 'entity.id': '108', - }, - { - 'entity.lastSeenTimestamp': '2023-05-08T00:40:17.226Z', - 'entity.type': 'service', - 'entity.displayName': 'Spider-Man 2099', - 'entity.id': '109', - }, - { - 'entity.lastSeenTimestamp': '2023-01-13T19:15:54.781Z', - 'entity.type': 'service', - 'entity.displayName': 'Spider-Ham', - 'entity.id': '110', - }, - { - 'entity.lastSeenTimestamp': '2024-09-02T15:35:26.309Z', - 'entity.type': 'container', - 'entity.displayName': 'Ultimate Spider-Man', - 'entity.id': '111', - }, - { - 'entity.lastSeenTimestamp': '2023-06-04T16:08:36.902Z', - 'entity.type': 'container', - 'entity.displayName': 'Spider-Man Noir', - 'entity.id': '112', - }, - { - 'entity.lastSeenTimestamp': '2023-02-12T13:28:29.732Z', - 'entity.type': 'service', - 'entity.displayName': 'Superior Spider-Man', - 'entity.id': '113', - }, - { - 'entity.lastSeenTimestamp': '2023-08-16T08:54:36.219Z', - 'entity.type': 'service', - 'entity.displayName': 'Agent Venom', - 'entity.id': '114', - }, - { - 'entity.lastSeenTimestamp': '2023-02-23T12:58:57.715Z', - 'entity.type': 'container', - 'entity.displayName': 'Venom', - 'entity.id': '115', - }, - { - 'entity.lastSeenTimestamp': '2023-06-19T18:17:35.424Z', - 'entity.type': 'container', - 'entity.displayName': 'Carnage', - 'entity.id': '116', - }, - { - 'entity.lastSeenTimestamp': '2024-05-02T11:58:44.239Z', - 'entity.type': 'service', - 'entity.displayName': 'Toxin', - 'entity.id': '117', - }, - { - 'entity.lastSeenTimestamp': '2023-12-27T14:15:59.641Z', - 'entity.type': 'host', - 'entity.displayName': 'Anti-Venom', - 'entity.id': '118', - }, - { - 'entity.lastSeenTimestamp': '2024-01-10T15:23:44.536Z', - 'entity.type': 'container', - 'entity.displayName': 'Morbius', - 'entity.id': '119', - }, - { - 'entity.lastSeenTimestamp': '2023-11-26T01:04:11.090Z', - 'entity.type': 'service', - 'entity.displayName': 'Kraven the Hunter', - 'entity.id': '120', - }, - { - 'entity.lastSeenTimestamp': '2024-02-21T04:11:13.221Z', - 'entity.type': 'container', - 'entity.displayName': 'The Lizard', - 'entity.id': '121', - }, - { - 'entity.lastSeenTimestamp': '2023-12-31T07:29:14.344Z', - 'entity.type': 'service', - 'entity.displayName': 'Sandman', - 'entity.id': '122', - }, - { - 'entity.lastSeenTimestamp': '2024-06-02T11:20:40.793Z', - 'entity.type': 'host', - 'entity.displayName': 'Rhino', - 'entity.id': '123', - }, - { - 'entity.lastSeenTimestamp': '2023-04-02T14:31:44.296Z', - 'entity.type': 'host', - 'entity.displayName': 'Shocker', - 'entity.id': '124', - }, - { - 'entity.lastSeenTimestamp': '2024-06-10T12:26:05.411Z', - 'entity.type': 'container', - 'entity.displayName': 'Vulture', - 'entity.id': '125', - }, - { - 'entity.lastSeenTimestamp': '2023-06-27T16:17:19.611Z', - 'entity.type': 'container', - 'entity.displayName': 'Mysterio', - 'entity.id': '126', - }, - { - 'entity.lastSeenTimestamp': '2023-08-29T04:54:25.898Z', - 'entity.type': 'service', - 'entity.displayName': 'Scorpion', - 'entity.id': '127', - }, - { - 'entity.lastSeenTimestamp': '2023-01-17T21:39:41.265Z', - 'entity.type': 'host', - 'entity.displayName': 'Chameleon', - 'entity.id': '128', - }, - { - 'entity.lastSeenTimestamp': '2023-06-07T03:03:11.032Z', - 'entity.type': 'host', - 'entity.displayName': 'Green Goblin', - 'entity.id': '129', - }, - { - 'entity.lastSeenTimestamp': '2023-05-19T19:18:21.005Z', - 'entity.type': 'service', - 'entity.displayName': 'Hobgoblin', - 'entity.id': '130', - }, - { - 'entity.lastSeenTimestamp': '2023-08-03T20:45:51.404Z', - 'entity.type': 'host', - 'entity.displayName': 'Demogoblin', - 'entity.id': '131', - }, - { - 'entity.lastSeenTimestamp': '2024-01-11T06:14:51.570Z', - 'entity.type': 'service', - 'entity.displayName': 'Red Goblin', - 'entity.id': '132', - }, - { - 'entity.lastSeenTimestamp': '2024-03-27T11:07:02.657Z', - 'entity.type': 'host', - 'entity.displayName': 'Doctor Octopus', - 'entity.id': '133', - }, - { - 'entity.lastSeenTimestamp': '2023-08-17T08:42:02.024Z', - 'entity.type': 'container', - 'entity.displayName': 'Electro', - 'entity.id': '134', - }, - { - 'entity.lastSeenTimestamp': '2023-07-02T16:02:17.438Z', - 'entity.type': 'container', - 'entity.displayName': 'Kingpin', - 'entity.id': '135', - }, - { - 'entity.lastSeenTimestamp': '2024-05-17T22:14:53.375Z', - 'entity.type': 'host', - 'entity.displayName': 'Tombstone', - 'entity.id': '136', - }, - { - 'entity.lastSeenTimestamp': '2023-05-30T09:26:45.647Z', - 'entity.type': 'service', - 'entity.displayName': 'Hammerhead', - 'entity.id': '137', - }, - { - 'entity.lastSeenTimestamp': '2024-09-08T03:21:22.494Z', - 'entity.type': 'host', - 'entity.displayName': 'Silvermane', - 'entity.id': '138', - }, - { - 'entity.lastSeenTimestamp': '2023-06-26T06:23:45.305Z', - 'entity.type': 'host', - 'entity.displayName': 'Hydro-Man', - 'entity.id': '139', - }, - { - 'entity.lastSeenTimestamp': '2024-08-15T13:29:01.603Z', - 'entity.type': 'host', - 'entity.displayName': 'Molten Man', - 'entity.id': '140', - }, - { - 'entity.lastSeenTimestamp': '2023-06-21T04:25:12.371Z', - 'entity.type': 'container', - 'entity.displayName': 'Morlun', - 'entity.id': '141', - }, - { - 'entity.lastSeenTimestamp': '2023-11-01T02:59:06.998Z', - 'entity.type': 'host', - 'entity.displayName': 'The Jackal', - 'entity.id': '142', - }, - { - 'entity.lastSeenTimestamp': '2023-06-25T15:27:39.801Z', - 'entity.type': 'service', - 'entity.displayName': 'Alistair Smythe', - 'entity.id': '143', - }, - { - 'entity.lastSeenTimestamp': '2023-12-07T19:13:02.711Z', - 'entity.type': 'service', - 'entity.displayName': 'The Beetle', - 'entity.id': '144', - }, - { - 'entity.lastSeenTimestamp': '2024-04-13T14:16:24.875Z', - 'entity.type': 'host', - 'entity.displayName': 'The Prowler', - 'entity.id': '145', - }, - { - 'entity.lastSeenTimestamp': '2023-11-02T20:25:05.117Z', - 'entity.type': 'host', - 'entity.displayName': 'Tarantula', - 'entity.id': '146', - }, - { - 'entity.lastSeenTimestamp': '2023-04-12T19:09:48.881Z', - 'entity.type': 'service', - 'entity.displayName': 'Black Tarantula', - 'entity.id': '147', - }, - { - 'entity.lastSeenTimestamp': '2024-01-25T01:37:16.115Z', - 'entity.type': 'host', - 'entity.displayName': 'White Tiger', - 'entity.id': '148', - }, - { - 'entity.lastSeenTimestamp': '2023-12-20T12:27:21.819Z', - 'entity.type': 'service', - 'entity.displayName': 'Nightcrawler', - 'entity.id': '149', - }, - { - 'entity.lastSeenTimestamp': '2024-06-11T05:30:01.226Z', - 'entity.type': 'container', - 'entity.displayName': 'Bishop', - 'entity.id': '150', - }, - { - 'entity.lastSeenTimestamp': '2023-09-24T00:18:40.137Z', - 'entity.type': 'service', - 'entity.displayName': 'Cable', - 'entity.id': '151', - }, - { - 'entity.lastSeenTimestamp': '2024-04-24T03:28:16.162Z', - 'entity.type': 'host', - 'entity.displayName': 'Domino', - 'entity.id': '152', - }, - { - 'entity.lastSeenTimestamp': '2023-04-08T07:23:33.921Z', - 'entity.type': 'host', - 'entity.displayName': 'Warpath', - 'entity.id': '153', - }, - { - 'entity.lastSeenTimestamp': '2023-04-12T23:26:45.533Z', - 'entity.type': 'service', - 'entity.displayName': 'Sunspot', - 'entity.id': '154', - }, - { - 'entity.lastSeenTimestamp': '2024-05-18T14:28:01.751Z', - 'entity.type': 'container', - 'entity.displayName': 'Cannonball', - 'entity.id': '155', - }, - { - 'entity.lastSeenTimestamp': '2023-03-14T17:08:06.243Z', - 'entity.type': 'container', - 'entity.displayName': 'Wolfsbane', - 'entity.id': '156', - }, - { - 'entity.lastSeenTimestamp': '2024-02-25T23:18:49.867Z', - 'entity.type': 'service', - 'entity.displayName': 'Magik', - 'entity.id': '157', - }, - { - 'entity.lastSeenTimestamp': '2024-07-14T14:31:58.080Z', - 'entity.type': 'container', - 'entity.displayName': 'Colossus', - 'entity.id': '158', - }, - { - 'entity.lastSeenTimestamp': '2023-05-09T22:32:41.723Z', - 'entity.type': 'container', - 'entity.displayName': 'Omega Red', - 'entity.id': '159', - }, - { - 'entity.lastSeenTimestamp': '2023-05-21T10:33:50.732Z', - 'entity.type': 'host', - 'entity.displayName': 'Juggernaut', - 'entity.id': '160', - }, - { - 'entity.lastSeenTimestamp': '2024-05-01T07:27:51.647Z', - 'entity.type': 'host', - 'entity.displayName': 'Sebastian Shaw', - 'entity.id': '161', - }, - { - 'entity.lastSeenTimestamp': '2024-01-25T09:47:54.565Z', - 'entity.type': 'service', - 'entity.displayName': 'Emma Frost', - 'entity.id': '162', - }, - { - 'entity.lastSeenTimestamp': '2023-10-25T15:51:18.513Z', - 'entity.type': 'host', - 'entity.displayName': 'Mystique', - 'entity.id': '163', - }, - { - 'entity.lastSeenTimestamp': '2023-03-27T07:26:04.804Z', - 'entity.type': 'service', - 'entity.displayName': 'Sabretooth', - 'entity.id': '164', - }, - { - 'entity.lastSeenTimestamp': '2024-07-22T15:29:51.446Z', - 'entity.type': 'host', - 'entity.displayName': 'Pyro', - 'entity.id': '165', - }, - { - 'entity.lastSeenTimestamp': '2024-06-26T09:09:57.169Z', - 'entity.type': 'host', - 'entity.displayName': 'Avalanche', - 'entity.id': '166', - }, - { - 'entity.lastSeenTimestamp': '2023-10-27T05:14:15.279Z', - 'entity.type': 'container', - 'entity.displayName': 'Destiny', - 'entity.id': '167', - }, - { - 'entity.lastSeenTimestamp': '2023-03-08T00:40:52.990Z', - 'entity.type': 'service', - 'entity.displayName': 'Forge', - 'entity.id': '168', - }, - { - 'entity.lastSeenTimestamp': '2023-11-05T16:40:30.510Z', - 'entity.type': 'host', - 'entity.displayName': 'Polaris', - 'entity.id': '169', - }, - { - 'entity.lastSeenTimestamp': '2024-08-12T05:04:27.632Z', - 'entity.type': 'service', - 'entity.displayName': 'Havok', - 'entity.id': '170', - }, - { - 'entity.lastSeenTimestamp': '2023-01-29T18:30:34.000Z', - 'entity.type': 'service', - 'entity.displayName': 'Multiple Man', - 'entity.id': '171', - }, - { - 'entity.lastSeenTimestamp': '2023-12-18T13:11:26.940Z', - 'entity.type': 'service', - 'entity.displayName': 'Strong Guy', - 'entity.id': '172', - }, - { - 'entity.lastSeenTimestamp': '2024-01-07T16:44:23.323Z', - 'entity.type': 'container', - 'entity.displayName': 'Feral', - 'entity.id': '173', - }, - { - 'entity.lastSeenTimestamp': '2023-03-19T22:38:34.493Z', - 'entity.type': 'container', - 'entity.displayName': 'Boom Boom', - 'entity.id': '174', - }, - { - 'entity.lastSeenTimestamp': '2023-05-31T07:23:57.500Z', - 'entity.type': 'container', - 'entity.displayName': 'Warlock', - 'entity.id': '175', - }, - { - 'entity.lastSeenTimestamp': '2024-07-11T01:57:10.851Z', - 'entity.type': 'host', - 'entity.displayName': 'Magus', - 'entity.id': '176', - }, - { - 'entity.lastSeenTimestamp': '2024-05-22T12:50:09.849Z', - 'entity.type': 'container', - 'entity.displayName': 'Blink', - 'entity.id': '177', - }, - { - 'entity.lastSeenTimestamp': '2023-10-20T11:56:09.004Z', - 'entity.type': 'service', - 'entity.displayName': 'Nocturne', - 'entity.id': '178', - }, - { - 'entity.lastSeenTimestamp': '2023-02-08T06:47:37.958Z', - 'entity.type': 'host', - 'entity.displayName': 'Morph', - 'entity.id': '179', - }, - { - 'entity.lastSeenTimestamp': '2023-09-16T01:14:58.701Z', - 'entity.type': 'host', - 'entity.displayName': 'Sunfire', - 'entity.id': '180', - }, - { - 'entity.lastSeenTimestamp': '2023-12-05T19:56:38.483Z', - 'entity.type': 'service', - 'entity.displayName': 'Thunderbird', - 'entity.id': '181', - }, - { - 'entity.lastSeenTimestamp': '2024-01-21T04:49:41.995Z', - 'entity.type': 'host', - 'entity.displayName': 'Banshee', - 'entity.id': '182', - }, - { - 'entity.lastSeenTimestamp': '2023-10-19T02:58:03.939Z', - 'entity.type': 'container', - 'entity.displayName': 'Syrin', - 'entity.id': '183', - }, - { - 'entity.lastSeenTimestamp': '2023-05-21T14:13:08.847Z', - 'entity.type': 'host', - 'entity.displayName': 'Moira MacTaggert', - 'entity.id': '184', - }, - { - 'entity.lastSeenTimestamp': '2024-02-09T05:57:59.984Z', - 'entity.type': 'container', - 'entity.displayName': 'Angel', - 'entity.id': '185', - }, - { - 'entity.lastSeenTimestamp': '2024-05-05T04:42:36.419Z', - 'entity.type': 'service', - 'entity.displayName': 'Archangel', - 'entity.id': '186', - }, - { - 'entity.lastSeenTimestamp': '2024-02-10T11:42:58.833Z', - 'entity.type': 'service', - 'entity.displayName': 'Iceman', - 'entity.id': '187', - }, - { - 'entity.lastSeenTimestamp': '2024-07-25T19:55:46.838Z', - 'entity.type': 'host', - 'entity.displayName': 'Beast', - 'entity.id': '188', - }, - { - 'entity.lastSeenTimestamp': '2024-09-11T05:07:10.339Z', - 'entity.type': 'host', - 'entity.displayName': 'Nightcrawler', - 'entity.id': '189', - }, - { - 'entity.lastSeenTimestamp': '2023-10-19T15:59:49.360Z', - 'entity.type': 'service', - 'entity.displayName': 'Phoenix', - 'entity.id': '190', - }, - { - 'entity.lastSeenTimestamp': '2024-09-07T17:32:26.019Z', - 'entity.type': 'host', - 'entity.displayName': 'X-Man', - 'entity.id': '191', - }, - { - 'entity.lastSeenTimestamp': '2023-07-13T12:49:11.603Z', - 'entity.type': 'container', - 'entity.displayName': 'Cable', - 'entity.id': '192', - }, - { - 'entity.lastSeenTimestamp': '2024-05-19T21:32:30.970Z', - 'entity.type': 'service', - 'entity.displayName': 'Deadpool', - 'entity.id': '193', - }, - { - 'entity.lastSeenTimestamp': '2023-12-12T00:33:27.870Z', - 'entity.type': 'host', - 'entity.displayName': 'Domino', - 'entity.id': '194', - }, - { - 'entity.lastSeenTimestamp': '2023-08-26T18:34:55.709Z', - 'entity.type': 'host', - 'entity.displayName': 'Shatterstar', - 'entity.id': '195', - }, - { - 'entity.lastSeenTimestamp': '2024-08-05T13:02:27.932Z', - 'entity.type': 'service', - 'entity.displayName': 'Warpath', - 'entity.id': '196', - }, - { - 'entity.lastSeenTimestamp': '2023-08-08T08:09:37.053Z', - 'entity.type': 'service', - 'entity.displayName': 'Rictor', - 'entity.id': '197', - }, - { - 'entity.lastSeenTimestamp': '2024-07-18T17:17:22.628Z', - 'entity.type': 'service', - 'entity.displayName': 'Boom Boom', - 'entity.id': '198', - }, - { - 'entity.lastSeenTimestamp': '2023-06-19T20:45:15.240Z', - 'entity.type': 'host', - 'entity.displayName': 'Magik', - 'entity.id': '199', - }, - { - 'entity.lastSeenTimestamp': '2023-07-29T15:18:44.936Z', - 'entity.type': 'container', - 'entity.displayName': 'Cannonball', - 'entity.id': '200', - }, - { - 'entity.lastSeenTimestamp': '2023-02-08T01:26:18.603Z', - 'entity.type': 'host', - 'entity.displayName': 'Sunspot', - 'entity.id': '201', - }, - { - 'entity.lastSeenTimestamp': '2023-02-22T16:06:39.387Z', - 'entity.type': 'service', - 'entity.displayName': 'Banshee', - 'entity.id': '202', - }, - { - 'entity.lastSeenTimestamp': '2023-04-27T03:32:37.015Z', - 'entity.type': 'host', - 'entity.displayName': 'Thunderbird', - 'entity.id': '203', - }, - { - 'entity.lastSeenTimestamp': '2023-09-08T13:07:04.895Z', - 'entity.type': 'service', - 'entity.displayName': 'X-23', - 'entity.id': '204', - }, - { - 'entity.lastSeenTimestamp': '2024-02-08T06:28:33.208Z', - 'entity.type': 'container', - 'entity.displayName': 'Daken', - 'entity.id': '205', - }, - { - 'entity.lastSeenTimestamp': '2024-01-19T19:28:19.416Z', - 'entity.type': 'host', - 'entity.displayName': 'Laura Kinney', - 'entity.id': '206', - }, - { - 'entity.lastSeenTimestamp': '2024-01-29T07:33:26.920Z', - 'entity.type': 'service', - 'entity.displayName': 'Jubilee', - 'entity.id': '207', - }, - { - 'entity.lastSeenTimestamp': '2023-02-20T10:19:34.322Z', - 'entity.type': 'host', - 'entity.displayName': 'Stepford Cuckoos', - 'entity.id': '208', - }, - { - 'entity.lastSeenTimestamp': '2024-06-03T03:31:08.704Z', - 'entity.type': 'service', - 'entity.displayName': 'Fantomex', - 'entity.id': '209', - }, - { - 'entity.lastSeenTimestamp': '2023-10-30T18:18:12.254Z', - 'entity.type': 'container', - 'entity.displayName': 'Marrow', - 'entity.id': '210', - }, - { - 'entity.lastSeenTimestamp': '2024-03-19T23:47:02.611Z', - 'entity.type': 'service', - 'entity.displayName': 'Pixie', - 'entity.id': '211', - }, - { - 'entity.lastSeenTimestamp': '2023-08-08T06:03:05.326Z', - 'entity.type': 'host', - 'entity.displayName': 'Armor', - 'entity.id': '212', - }, - { - 'entity.lastSeenTimestamp': '2023-04-05T11:25:37.426Z', - 'entity.type': 'service', - 'entity.displayName': 'Gentle', - 'entity.id': '213', - }, - { - 'entity.lastSeenTimestamp': '2023-01-10T22:18:30.812Z', - 'entity.type': 'container', - 'entity.displayName': 'Anole', - 'entity.id': '214', - }, - { - 'entity.lastSeenTimestamp': '2024-07-17T06:09:51.763Z', - 'entity.type': 'host', - 'entity.displayName': 'Rockslide', - 'entity.id': '215', - }, - { - 'entity.lastSeenTimestamp': '2024-02-02T00:44:56.270Z', - 'entity.type': 'host', - 'entity.displayName': 'Dust', - 'entity.id': '216', - }, - { - 'entity.lastSeenTimestamp': '2023-03-09T19:37:54.235Z', - 'entity.type': 'host', - 'entity.displayName': 'Mercury', - 'entity.id': '217', - }, - { - 'entity.lastSeenTimestamp': '2024-06-14T09:51:29.579Z', - 'entity.type': 'service', - 'entity.displayName': 'Surge', - 'entity.id': '218', - }, - { - 'entity.lastSeenTimestamp': '2024-03-12T17:28:48.254Z', - 'entity.type': 'host', - 'entity.displayName': 'Hellion', - 'entity.id': '219', - }, - { - 'entity.lastSeenTimestamp': '2023-04-09T07:19:02.429Z', - 'entity.type': 'service', - 'entity.displayName': 'Elixir', - 'entity.id': '220', - }, - { - 'entity.lastSeenTimestamp': '2024-05-10T08:28:21.025Z', - 'entity.type': 'host', - 'entity.displayName': 'X-23', - 'entity.id': '221', - }, - { - 'entity.lastSeenTimestamp': '2023-05-01T16:23:41.343Z', - 'entity.type': 'host', - 'entity.displayName': 'Prodigy', - 'entity.id': '222', - }, - { - 'entity.lastSeenTimestamp': '2023-02-03T07:17:47.909Z', - 'entity.type': 'container', - 'entity.displayName': 'Blindfold', - 'entity.id': '223', - }, - { - 'entity.lastSeenTimestamp': '2023-06-15T00:56:00.094Z', - 'entity.type': 'service', - 'entity.displayName': 'Ink', - 'entity.id': '224', - }, - { - 'entity.lastSeenTimestamp': '2024-04-28T22:32:11.149Z', - 'entity.type': 'container', - 'entity.displayName': 'Goldballs', - 'entity.id': '225', - }, - { - 'entity.lastSeenTimestamp': '2023-10-14T04:34:56.973Z', - 'entity.type': 'service', - 'entity.displayName': 'Magneto', - 'entity.id': '226', - }, - { - 'entity.lastSeenTimestamp': '2024-08-20T08:01:12.156Z', - 'entity.type': 'host', - 'entity.displayName': 'Juggernaut', - 'entity.id': '227', - }, - { - 'entity.lastSeenTimestamp': '2023-12-31T15:27:41.198Z', - 'entity.type': 'host', - 'entity.displayName': 'Mystique', - 'entity.id': '228', - }, - { - 'entity.lastSeenTimestamp': '2024-03-06T04:31:14.001Z', - 'entity.type': 'service', - 'entity.displayName': 'Sabretooth', - 'entity.id': '229', - }, - { - 'entity.lastSeenTimestamp': '2024-03-26T05:07:12.552Z', - 'entity.type': 'host', - 'entity.displayName': 'Toad', - 'entity.id': '230', - }, - { - 'entity.lastSeenTimestamp': '2024-05-20T17:34:56.098Z', - 'entity.type': 'service', - 'entity.displayName': 'Pyro', - 'entity.id': '231', - }, - { - 'entity.lastSeenTimestamp': '2023-04-12T16:53:27.530Z', - 'entity.type': 'host', - 'entity.displayName': 'Avalanche', - 'entity.id': '232', - }, - { - 'entity.lastSeenTimestamp': '2023-02-21T16:26:36.731Z', - 'entity.type': 'container', - 'entity.displayName': 'Blob', - 'entity.id': '233', - }, - { - 'entity.lastSeenTimestamp': '2023-03-23T03:52:18.017Z', - 'entity.type': 'host', - 'entity.displayName': 'Sauron', - 'entity.id': '234', - }, - { - 'entity.lastSeenTimestamp': '2024-04-10T21:31:37.929Z', - 'entity.type': 'container', - 'entity.displayName': 'Omega Red', - 'entity.id': '235', - }, - { - 'entity.lastSeenTimestamp': '2024-02-28T14:35:09.897Z', - 'entity.type': 'service', - 'entity.displayName': 'Mr. Sinister', - 'entity.id': '236', - }, - { - 'entity.lastSeenTimestamp': '2024-01-24T09:05:06.205Z', - 'entity.type': 'host', - 'entity.displayName': 'Apocalypse', - 'entity.id': '237', - }, - { - 'entity.lastSeenTimestamp': '2024-02-05T23:30:26.586Z', - 'entity.type': 'service', - 'entity.displayName': 'Genesis', - 'entity.id': '238', - }, - { - 'entity.lastSeenTimestamp': '2023-04-18T11:46:41.466Z', - 'entity.type': 'service', - 'entity.displayName': 'Archangel', - 'entity.id': '239', - }, - { - 'entity.lastSeenTimestamp': '2023-02-28T02:10:52.053Z', - 'entity.type': 'host', - 'entity.displayName': 'Holocaust', - 'entity.id': '240', - }, - { - 'entity.lastSeenTimestamp': '2024-04-03T12:19:13.947Z', - 'entity.type': 'host', - 'entity.displayName': 'Onslaught', - 'entity.id': '241', - }, - { - 'entity.lastSeenTimestamp': '2023-01-20T01:37:07.489Z', - 'entity.type': 'host', - 'entity.displayName': 'Exodus', - 'entity.id': '242', - }, - { - 'entity.lastSeenTimestamp': '2023-10-09T07:32:39.074Z', - 'entity.type': 'container', - 'entity.displayName': 'Gambit', - 'entity.id': '243', - }, - { - 'entity.lastSeenTimestamp': '2024-07-21T15:09:00.494Z', - 'entity.type': 'host', - 'entity.displayName': 'Rogue', - 'entity.id': '244', - }, - { - 'entity.lastSeenTimestamp': '2024-08-03T14:58:05.875Z', - 'entity.type': 'service', - 'entity.displayName': 'Magneto', - 'entity.id': '245', - }, - { - 'entity.lastSeenTimestamp': '2024-02-08T04:32:33.334Z', - 'entity.type': 'container', - 'entity.displayName': 'Longshot', - 'entity.id': '246', - }, - { - 'entity.lastSeenTimestamp': '2023-03-18T10:37:49.383Z', - 'entity.type': 'service', - 'entity.displayName': 'Dazzler', - 'entity.id': '247', - }, - { - 'entity.lastSeenTimestamp': '2024-07-11T17:35:31.669Z', - 'entity.type': 'service', - 'entity.displayName': 'Forge', - 'entity.id': '248', - }, - { - 'entity.lastSeenTimestamp': '2024-08-23T15:01:17.593Z', - 'entity.type': 'host', - 'entity.displayName': 'Mojo', - 'entity.id': '249', - }, - { - 'entity.lastSeenTimestamp': '2023-06-27T15:34:23.105Z', - 'entity.type': 'service', - 'entity.displayName': 'Spiral', - 'entity.id': '250', - }, - { - 'entity.lastSeenTimestamp': '2024-03-19T08:06:40.658Z', - 'entity.type': 'container', - 'entity.displayName': 'Warlock', - 'entity.id': '251', - }, - { - 'entity.lastSeenTimestamp': '2023-08-19T12:10:02.477Z', - 'entity.type': 'container', - 'entity.displayName': 'Magus', - 'entity.id': '252', - }, - { - 'entity.lastSeenTimestamp': '2024-05-30T12:20:06.653Z', - 'entity.type': 'service', - 'entity.displayName': 'Douglock', - 'entity.id': '253', - }, - { - 'entity.lastSeenTimestamp': '2023-05-31T17:48:07.719Z', - 'entity.type': 'service', - 'entity.displayName': 'Shatterstar', - 'entity.id': '254', - }, - { - 'entity.lastSeenTimestamp': '2023-12-10T05:35:40.666Z', - 'entity.type': 'service', - 'entity.displayName': 'Rictor', - 'entity.id': '255', - }, - { - 'entity.lastSeenTimestamp': '2024-02-02T22:18:47.168Z', - 'entity.type': 'host', - 'entity.displayName': 'Domino', - 'entity.id': '256', - }, - { - 'entity.lastSeenTimestamp': '2024-01-07T22:07:45.968Z', - 'entity.type': 'container', - 'entity.displayName': 'Cable', - 'entity.id': '257', - }, - { - 'entity.lastSeenTimestamp': '2023-01-15T11:22:54.155Z', - 'entity.type': 'host', - 'entity.displayName': 'Hope Summers', - 'entity.id': '258', - }, - { - 'entity.lastSeenTimestamp': '2023-03-26T13:56:10.553Z', - 'entity.type': 'service', - 'entity.displayName': 'Deadpool', - 'entity.id': '259', - }, - { - 'entity.lastSeenTimestamp': '2023-08-15T19:17:34.583Z', - 'entity.type': 'service', - 'entity.displayName': 'X-23', - 'entity.id': '260', - }, - { - 'entity.lastSeenTimestamp': '2024-06-26T09:02:40.512Z', - 'entity.type': 'host', - 'entity.displayName': 'Daken', - 'entity.id': '261', - }, - { - 'entity.lastSeenTimestamp': '2024-07-07T09:01:04.091Z', - 'entity.type': 'host', - 'entity.displayName': 'Wolverine', - 'entity.id': '262', - }, - { - 'entity.lastSeenTimestamp': '2023-10-15T22:25:29.643Z', - 'entity.type': 'service', - 'entity.displayName': 'Old Man Logan', - 'entity.id': '263', - }, - { - 'entity.lastSeenTimestamp': '2024-07-07T04:51:19.761Z', - 'entity.type': 'container', - 'entity.displayName': 'The Maker', - 'entity.id': '264', - }, - { - 'entity.lastSeenTimestamp': '2024-03-13T14:00:51.289Z', - 'entity.type': 'container', - 'entity.displayName': 'Ultimate Thor', - 'entity.id': '265', - }, - { - 'entity.lastSeenTimestamp': '2023-07-23T10:13:07.651Z', - 'entity.type': 'service', - 'entity.displayName': 'Ultimate Iron Man', - 'entity.id': '266', - }, - { - 'entity.lastSeenTimestamp': '2023-08-20T07:09:20.148Z', - 'entity.type': 'container', - 'entity.displayName': 'Ultimate Hulk', - 'entity.id': '267', - }, - { - 'entity.lastSeenTimestamp': '2024-09-08T10:53:13.256Z', - 'entity.type': 'service', - 'entity.displayName': 'Ultimate Captain America', - 'entity.id': '268', - }, - { - 'entity.lastSeenTimestamp': '2024-09-15T03:57:28.175Z', - 'entity.type': 'container', - 'entity.displayName': - 'Sed dignissim libero a diam sagittis, in convallis leo pellentesque. Cras ut sapien sed lacus scelerisque vehicula. Pellentesque at purus pulvinar, mollis justo hendrerit, pharetra purus. Morbi dapibus, augue et volutpat ultricies, neque quam sollicitudin mauris, vitae luctus ex libero id erat. Suspendisse risus lectus, scelerisque vel odio sed.', - 'entity.id': '269', - alertsCount: 4, - }, - { - 'entity.lastSeenTimestamp': '2023-10-22T13:49:53.092Z', - 'entity.type': 'host', - 'entity.displayName': 'Silk', - 'entity.id': '270', - }, - { - 'entity.lastSeenTimestamp': '2023-01-13T00:36:25.773Z', - 'entity.type': 'host', - 'entity.displayName': 'Scarlet Spider', - 'entity.id': '271', - }, - { - 'entity.lastSeenTimestamp': '2023-12-10T19:31:42.994Z', - 'entity.type': 'service', - 'entity.displayName': 'Ben Reilly', - 'entity.id': '272', - }, - { - 'entity.lastSeenTimestamp': '2023-01-17T09:49:30.447Z', - 'entity.type': 'service', - 'entity.displayName': 'Miles Morales', - 'entity.id': '273', - }, - { - 'entity.lastSeenTimestamp': '2023-01-02T18:45:44.012Z', - 'entity.type': 'container', - 'entity.displayName': 'Spider-Ham', - 'entity.id': '274', - }, - { - 'entity.lastSeenTimestamp': '2023-06-28T22:50:08.414Z', - 'entity.type': 'container', - 'entity.displayName': 'Agent Venom', - 'entity.id': '275', - }, - { - 'entity.lastSeenTimestamp': '2023-03-30T17:01:35.995Z', - 'entity.type': 'service', - 'entity.displayName': 'Anti-Venom', - 'entity.id': '276', - }, - { - 'entity.lastSeenTimestamp': '2023-06-11T05:23:11.367Z', - 'entity.type': 'host', - 'entity.displayName': 'Toxin', - 'entity.id': '277', - }, - { - 'entity.lastSeenTimestamp': '2023-07-22T15:27:17.077Z', - 'entity.type': 'service', - 'entity.displayName': 'Morbius', - 'entity.id': '278', - }, - { - 'entity.lastSeenTimestamp': '2024-01-26T11:19:34.147Z', - 'entity.type': 'host', - 'entity.displayName': 'Kraven the Hunter', - 'entity.id': '279', - }, - { - 'entity.lastSeenTimestamp': '2024-06-18T09:03:01.111Z', - 'entity.type': 'container', - 'entity.displayName': 'Doctor Octopus', - 'entity.id': '280', - }, - { - 'entity.lastSeenTimestamp': '2024-07-27T14:08:12.583Z', - 'entity.type': 'container', - 'entity.displayName': 'Green Goblin', - 'entity.id': '281', - }, - { - 'entity.lastSeenTimestamp': '2023-01-12T01:38:45.243Z', - 'entity.type': 'host', - 'entity.displayName': 'Electro', - 'entity.id': '282', - }, - { - 'entity.lastSeenTimestamp': '2024-04-19T05:33:59.289Z', - 'entity.type': 'container', - 'entity.displayName': 'Rhino', - 'entity.id': '283', - }, - { - 'entity.lastSeenTimestamp': '2023-04-13T22:06:02.389Z', - 'entity.type': 'service', - 'entity.displayName': 'Shocker', - 'entity.id': '284', - }, - { - 'entity.lastSeenTimestamp': '2023-01-26T15:36:08.782Z', - 'entity.type': 'host', - 'entity.displayName': 'Vulture', - 'entity.id': '285', - }, - { - 'entity.lastSeenTimestamp': '2023-11-11T19:54:14.523Z', - 'entity.type': 'container', - 'entity.displayName': 'Sandman', - 'entity.id': '286', - }, - { - 'entity.lastSeenTimestamp': '2023-12-06T06:20:06.995Z', - 'entity.type': 'host', - 'entity.displayName': 'Mysterio', - 'entity.id': '287', - }, - { - 'entity.lastSeenTimestamp': '2023-07-23T04:30:35.686Z', - 'entity.type': 'service', - 'entity.displayName': 'Black Cat', - 'entity.id': '288', - }, - { - 'entity.lastSeenTimestamp': '2023-01-18T03:09:26.047Z', - 'entity.type': 'host', - 'entity.displayName': 'Silver Sable', - 'entity.id': '289', - }, - { - 'entity.lastSeenTimestamp': '2024-06-08T12:42:08.485Z', - 'entity.type': 'service', - 'entity.displayName': 'Chameleon', - 'entity.id': '290', - }, - { - 'entity.lastSeenTimestamp': '2023-08-18T03:34:28.230Z', - 'entity.type': 'container', - 'entity.displayName': 'Hammerhead', - 'entity.id': '291', - }, - { - 'entity.lastSeenTimestamp': '2024-04-13T01:42:03.890Z', - 'entity.type': 'container', - 'entity.displayName': 'Tombstone', - 'entity.id': '292', - }, - { - 'entity.lastSeenTimestamp': '2023-11-21T17:39:56.066Z', - 'entity.type': 'container', - 'entity.displayName': 'Alistair Smythe', - 'entity.id': '293', - }, - { - 'entity.lastSeenTimestamp': '2024-02-29T04:45:41.113Z', - 'entity.type': 'host', - 'entity.displayName': 'The Beetle', - 'entity.id': '294', - }, - { - 'entity.lastSeenTimestamp': '2024-08-12T07:40:35.827Z', - 'entity.type': 'host', - 'entity.displayName': 'The Prowler', - 'entity.id': '295', - }, - { - 'entity.lastSeenTimestamp': '2023-11-27T23:09:49.629Z', - 'entity.type': 'service', - 'entity.displayName': 'Scorpion', - 'entity.id': '296', - }, - { - 'entity.lastSeenTimestamp': '2024-08-29T21:24:37.304Z', - 'entity.type': 'container', - 'entity.displayName': 'Jackal', - 'entity.id': '297', - }, - { - 'entity.lastSeenTimestamp': '2023-03-25T03:08:42.970Z', - 'entity.type': 'container', - 'entity.displayName': 'Morlun', - 'entity.id': '298', - }, - { - 'entity.lastSeenTimestamp': '2023-12-12T01:01:52.801Z', - 'entity.type': 'container', - 'entity.displayName': 'Lizard', - 'entity.id': '299', - }, - { - 'entity.lastSeenTimestamp': '2024-02-22T02:29:11.333Z', - 'entity.type': 'service', - 'entity.displayName': 'Kingpin', - 'entity.id': '300', - }, - { - 'entity.lastSeenTimestamp': '2024-09-03T19:31:38.700Z', - 'entity.type': 'host', - 'entity.displayName': 'Carnage', - 'entity.id': '301', - }, - { - 'entity.lastSeenTimestamp': '2023-04-09T17:55:20.565Z', - 'entity.type': 'container', - 'entity.displayName': 'Norman Osborn', - 'entity.id': '302', - }, - { - 'entity.lastSeenTimestamp': '2023-11-15T11:23:39.657Z', - 'entity.type': 'container', - 'entity.displayName': 'Harry Osborn', - 'entity.id': '303', - }, - { - 'entity.lastSeenTimestamp': '2024-08-16T08:14:11.415Z', - 'entity.type': 'service', - 'entity.displayName': 'Hobgoblin', - 'entity.id': '304', - }, - { - 'entity.lastSeenTimestamp': '2023-04-09T06:48:50.111Z', - 'entity.type': 'container', - 'entity.displayName': 'Phil Urich', - 'entity.id': '305', - }, - { - 'entity.lastSeenTimestamp': '2023-10-07T15:00:25.174Z', - 'entity.type': 'host', - 'entity.displayName': 'Demogoblin', - 'entity.id': '306', - }, - { - 'entity.lastSeenTimestamp': '2024-05-04T22:13:00.266Z', - 'entity.type': 'container', - 'entity.displayName': 'Red Goblin', - 'entity.id': '307', - }, - { - 'entity.lastSeenTimestamp': '2024-04-04T23:46:04.650Z', - 'entity.type': 'container', - 'entity.displayName': 'Doctor Octopus', - 'entity.id': '308', - }, - { - 'entity.lastSeenTimestamp': '2023-03-09T03:17:41.028Z', - 'entity.type': 'container', - 'entity.displayName': 'Otto Octavius', - 'entity.id': '309', - }, - { - 'entity.lastSeenTimestamp': '2023-02-15T01:52:08.165Z', - 'entity.type': 'service', - 'entity.displayName': 'Spider-Slayer', - 'entity.id': '310', - }, - { - 'entity.lastSeenTimestamp': '2024-05-18T16:03:17.334Z', - 'entity.type': 'container', - 'entity.displayName': 'The Spot', - 'entity.id': '311', - }, - { - 'entity.lastSeenTimestamp': '2023-10-24T01:14:40.519Z', - 'entity.type': 'host', - 'entity.displayName': 'White Tiger', - 'entity.id': '312', - }, - { - 'entity.lastSeenTimestamp': '2023-11-25T03:29:54.122Z', - 'entity.type': 'container', - 'entity.displayName': 'Kang', - 'entity.id': '313', - }, - { - 'entity.lastSeenTimestamp': '2023-03-10T14:39:44.761Z', - 'entity.type': 'container', - 'entity.displayName': 'Baron Zemo', - 'entity.id': '314', - }, - { - 'entity.lastSeenTimestamp': '2023-05-02T09:25:50.743Z', - 'entity.type': 'host', - 'entity.displayName': 'Red Skull', - 'entity.id': '315', - }, - { - 'entity.lastSeenTimestamp': '2023-04-09T14:57:15.653Z', - 'entity.type': 'container', - 'entity.displayName': 'MODOK', - 'entity.id': '316', - }, - { - 'entity.lastSeenTimestamp': '2023-12-02T10:21:33.045Z', - 'entity.type': 'service', - 'entity.displayName': 'Taskmaster', - 'entity.id': '317', - }, - { - 'entity.lastSeenTimestamp': '2023-09-26T12:18:47.857Z', - 'entity.type': 'service', - 'entity.displayName': 'Ultron', - 'entity.id': '318', - }, - { - 'entity.lastSeenTimestamp': '2023-06-29T22:13:32.744Z', - 'entity.type': 'container', - 'entity.displayName': 'Crossbones', - 'entity.id': '319', - }, - { - 'entity.lastSeenTimestamp': '2023-04-29T16:04:40.552Z', - 'entity.type': 'service', - 'entity.displayName': 'Madame Hydra', - 'entity.id': '320', - }, - { - 'entity.lastSeenTimestamp': '2023-07-26T05:34:55.857Z', - 'entity.type': 'host', - 'entity.displayName': 'The Leader', - 'entity.id': '321', - }, - { - 'entity.lastSeenTimestamp': '2023-05-23T13:21:34.771Z', - 'entity.type': 'service', - 'entity.displayName': 'Abomination', - 'entity.id': '322', - }, - { - 'entity.lastSeenTimestamp': '2024-05-06T22:15:26.389Z', - 'entity.type': 'container', - 'entity.displayName': 'The Mandarin', - 'entity.id': '323', - }, - { - 'entity.lastSeenTimestamp': '2024-01-08T09:12:59.615Z', - 'entity.type': 'service', - 'entity.displayName': 'Fin Fang Foom', - 'entity.id': '324', - }, - { - 'entity.lastSeenTimestamp': '2023-07-07T15:39:12.867Z', - 'entity.type': 'container', - 'entity.displayName': 'Killmonger', - 'entity.id': '325', - }, - { - 'entity.lastSeenTimestamp': '2023-12-04T02:42:55.907Z', - 'entity.type': 'container', - 'entity.displayName': 'Ulysses Klaue', - 'entity.id': '326', - }, - { - 'entity.lastSeenTimestamp': '2024-01-01T10:14:42.258Z', - 'entity.type': 'container', - 'entity.displayName': 'The Collector', - 'entity.id': '327', - }, - { - 'entity.lastSeenTimestamp': '2024-07-21T02:20:14.626Z', - 'entity.type': 'container', - 'entity.displayName': 'The Grandmaster', - 'entity.id': '328', - }, - { - 'entity.lastSeenTimestamp': '2024-04-19T01:54:14.317Z', - 'entity.type': 'service', - 'entity.displayName': 'Thanos', - 'entity.id': '329', - }, - { - 'entity.lastSeenTimestamp': '2023-12-15T04:43:05.141Z', - 'entity.type': 'host', - 'entity.displayName': 'Darkseid', - 'entity.id': '330', - }, - { - 'entity.lastSeenTimestamp': '2023-06-20T14:32:29.968Z', - 'entity.type': 'service', - 'entity.displayName': 'Lex Luthor', - 'entity.id': '331', - }, - { - 'entity.lastSeenTimestamp': '2023-11-02T15:33:40.790Z', - 'entity.type': 'container', - 'entity.displayName': 'Bane', - 'entity.id': '332', - }, - { - 'entity.lastSeenTimestamp': '2024-06-09T08:34:20.039Z', - 'entity.type': 'host', - 'entity.displayName': 'Brainiac', - 'entity.id': '333', - }, - { - 'entity.lastSeenTimestamp': '2024-08-30T14:00:25.077Z', - 'entity.type': 'container', - 'entity.displayName': 'Doomsday', - 'entity.id': '334', - }, - { - 'entity.lastSeenTimestamp': '2024-02-26T18:03:06.283Z', - 'entity.type': 'service', - 'entity.displayName': 'General Zod', - 'entity.id': '335', - }, - { - 'entity.lastSeenTimestamp': '2023-10-30T05:16:19.508Z', - 'entity.type': 'host', - 'entity.displayName': "Ra's al Ghul", - 'entity.id': '336', - }, - { - 'entity.lastSeenTimestamp': '2023-04-05T20:09:22.332Z', - 'entity.type': 'host', - 'entity.displayName': 'Scarecrow', - 'entity.id': '337', - }, - { - 'entity.lastSeenTimestamp': '2023-06-09T06:46:09.887Z', - 'entity.type': 'service', - 'entity.displayName': 'The Joker', - 'entity.id': '338', - }, - { - 'entity.lastSeenTimestamp': '2023-04-26T15:02:13.202Z', - 'entity.type': 'host', - 'entity.displayName': 'Harley Quinn', - 'entity.id': '339', - }, - { - 'entity.lastSeenTimestamp': '2024-04-09T05:21:09.975Z', - 'entity.type': 'service', - 'entity.displayName': 'Poison Ivy', - 'entity.id': '340', - }, - { - 'entity.lastSeenTimestamp': '2023-06-05T04:53:00.171Z', - 'entity.type': 'service', - 'entity.displayName': 'The Riddler', - 'entity.id': '341', - }, - { - 'entity.lastSeenTimestamp': '2024-03-07T01:23:08.698Z', - 'entity.type': 'host', - 'entity.displayName': 'Penguin', - 'entity.id': '342', - }, - { - 'entity.lastSeenTimestamp': '2024-05-17T13:08:12.434Z', - 'entity.type': 'container', - 'entity.displayName': 'Two-Face', - 'entity.id': '343', - }, - { - 'entity.lastSeenTimestamp': '2024-03-13T16:39:26.987Z', - 'entity.type': 'service', - 'entity.displayName': 'Mr. Freeze', - 'entity.id': '344', - }, - { - 'entity.lastSeenTimestamp': '2024-01-01T06:31:32.470Z', - 'entity.type': 'host', - 'entity.displayName': 'Clayface', - 'entity.id': '345', - }, - { - 'entity.lastSeenTimestamp': '2024-06-24T16:27:01.156Z', - 'entity.type': 'service', - 'entity.displayName': 'Hush', - 'entity.id': '346', - }, - { - 'entity.lastSeenTimestamp': '2023-10-19T14:35:47.544Z', - 'entity.type': 'host', - 'entity.displayName': 'Black Mask', - 'entity.id': '347', - }, - { - 'entity.lastSeenTimestamp': '2023-10-24T13:57:07.539Z', - 'entity.type': 'host', - 'entity.displayName': 'Killer Croc', - 'entity.id': '348', - }, - { - 'entity.lastSeenTimestamp': '2023-02-19T09:40:44.538Z', - 'entity.type': 'service', - 'entity.displayName': 'Deathstroke', - 'entity.id': '349', - }, - { - 'entity.lastSeenTimestamp': '2023-03-25T19:22:45.889Z', - 'entity.type': 'service', - 'entity.displayName': 'Deadshot', - 'entity.id': '350', - }, - { - 'entity.lastSeenTimestamp': '2024-06-08T03:10:02.475Z', - 'entity.type': 'container', - 'entity.displayName': 'Amanda Waller', - 'entity.id': '351', - }, - { - 'entity.lastSeenTimestamp': '2023-01-04T03:49:07.210Z', - 'entity.type': 'host', - 'entity.displayName': 'Captain Boomerang', - 'entity.id': '352', - }, - { - 'entity.lastSeenTimestamp': '2023-04-10T20:53:14.367Z', - 'entity.type': 'host', - 'entity.displayName': 'Katana', - 'entity.id': '353', - }, - { - 'entity.lastSeenTimestamp': '2024-04-25T09:42:55.170Z', - 'entity.type': 'host', - 'entity.displayName': 'El Diablo', - 'entity.id': '354', - }, - { - 'entity.lastSeenTimestamp': '2024-05-10T00:44:03.472Z', - 'entity.type': 'host', - 'entity.displayName': 'Enchantress', - 'entity.id': '355', - }, - { - 'entity.lastSeenTimestamp': '2024-02-16T03:47:56.021Z', - 'entity.type': 'service', - 'entity.displayName': 'Rick Flag', - 'entity.id': '356', - }, - { - 'entity.lastSeenTimestamp': '2023-09-30T16:45:27.670Z', - 'entity.type': 'host', - 'entity.displayName': 'King Shark', - 'entity.id': '357', - }, - { - 'entity.lastSeenTimestamp': '2023-10-14T03:04:21.380Z', - 'entity.type': 'host', - 'entity.displayName': 'Peacemaker', - 'entity.id': '358', - }, - { - 'entity.lastSeenTimestamp': '2023-06-27T20:42:18.732Z', - 'entity.type': 'host', - 'entity.displayName': 'Bloodsport', - 'entity.id': '359', - }, - { - 'entity.lastSeenTimestamp': '2024-05-25T22:56:14.675Z', - 'entity.type': 'container', - 'entity.displayName': 'Weasel', - 'entity.id': '360', - }, - { - 'entity.lastSeenTimestamp': '2024-05-15T05:34:39.704Z', - 'entity.type': 'container', - 'entity.displayName': 'Javelin', - 'entity.id': '361', - }, - { - 'entity.lastSeenTimestamp': '2024-07-18T13:40:24.040Z', - 'entity.type': 'container', - 'entity.displayName': 'Ratcatcher', - 'entity.id': '362', - }, - { - 'entity.lastSeenTimestamp': '2023-08-31T03:02:00.545Z', - 'entity.type': 'container', - 'entity.displayName': 'T.D.K.', - 'entity.id': '363', - }, - { - 'entity.lastSeenTimestamp': '2024-08-27T11:13:19.374Z', - 'entity.type': 'container', - 'entity.displayName': 'Doctor Fate', - 'entity.id': '364', - }, - { - 'entity.lastSeenTimestamp': '2023-08-29T06:47:41.545Z', - 'entity.type': 'container', - 'entity.displayName': 'Hawkman', - 'entity.id': '365', - }, - { - 'entity.lastSeenTimestamp': '2024-04-30T00:01:35.041Z', - 'entity.type': 'service', - 'entity.displayName': 'Hawkgirl', - 'entity.id': '366', - }, - { - 'entity.lastSeenTimestamp': '2024-01-24T01:02:59.317Z', - 'entity.type': 'container', - 'entity.displayName': 'Black Adam', - 'entity.id': '367', - }, - { - 'entity.lastSeenTimestamp': '2023-11-08T14:30:16.054Z', - 'entity.type': 'service', - 'entity.displayName': 'Atom Smasher', - 'entity.id': '368', - }, - { - 'entity.lastSeenTimestamp': '2024-08-02T05:40:07.271Z', - 'entity.type': 'host', - 'entity.displayName': 'Cyclone', - 'entity.id': '369', - }, - { - 'entity.lastSeenTimestamp': '2024-03-24T19:11:13.807Z', - 'entity.type': 'host', - 'entity.displayName': 'Stargirl', - 'entity.id': '370', - }, - { - 'entity.lastSeenTimestamp': '2024-01-25T19:31:31.536Z', - 'entity.type': 'host', - 'entity.displayName': 'Hourman', - 'entity.id': '371', - }, - { - 'entity.lastSeenTimestamp': '2024-05-20T22:09:46.339Z', - 'entity.type': 'service', - 'entity.displayName': 'Wildcat', - 'entity.id': '372', - }, - { - 'entity.lastSeenTimestamp': '2023-07-31T01:51:08.575Z', - 'entity.type': 'host', - 'entity.displayName': 'Green Arrow', - 'entity.id': '373', - }, - { - 'entity.lastSeenTimestamp': '2024-03-23T22:01:53.447Z', - 'entity.type': 'container', - 'entity.displayName': 'Speedy', - 'entity.id': '374', - }, - { - 'entity.lastSeenTimestamp': '2024-02-11T22:26:31.584Z', - 'entity.type': 'service', - 'entity.displayName': 'Arsenal', - 'entity.id': '375', - }, - { - 'entity.lastSeenTimestamp': '2024-04-06T12:30:22.601Z', - 'entity.type': 'service', - 'entity.displayName': 'Red Hood', - 'entity.id': '376', - }, - { - 'entity.lastSeenTimestamp': '2023-09-13T07:02:26.095Z', - 'entity.type': 'service', - 'entity.displayName': 'Batgirl', - 'entity.id': '377', - }, - { - 'entity.lastSeenTimestamp': '2024-07-07T22:22:48.331Z', - 'entity.type': 'container', - 'entity.displayName': 'Oracle', - 'entity.id': '378', - }, - { - 'entity.lastSeenTimestamp': '2024-08-09T21:51:59.774Z', - 'entity.type': 'host', - 'entity.displayName': 'Huntress', - 'entity.id': '379', - }, - { - 'entity.lastSeenTimestamp': '2024-02-04T21:15:45.848Z', - 'entity.type': 'service', - 'entity.displayName': 'Cassandra Cain', - 'entity.id': '380', - }, - { - 'entity.lastSeenTimestamp': '2023-07-23T14:22:33.033Z', - 'entity.type': 'host', - 'entity.displayName': 'Azrael', - 'entity.id': '381', - }, - { - 'entity.lastSeenTimestamp': '2024-09-04T05:28:23.197Z', - 'entity.type': 'container', - 'entity.displayName': 'Batwoman', - 'entity.id': '382', - }, - { - 'entity.lastSeenTimestamp': '2023-06-27T08:09:37.626Z', - 'entity.type': 'container', - 'entity.displayName': 'Stephanie Brown', - 'entity.id': '383', - }, - { - 'entity.lastSeenTimestamp': '2023-12-20T08:14:23.553Z', - 'entity.type': 'host', - 'entity.displayName': 'The Question', - 'entity.id': '384', - }, - { - 'entity.lastSeenTimestamp': '2024-03-17T00:19:48.826Z', - 'entity.type': 'container', - 'entity.displayName': 'Blue Beetle', - 'entity.id': '385', - }, - { - 'entity.lastSeenTimestamp': '2024-02-17T20:55:20.634Z', - 'entity.type': 'container', - 'entity.displayName': 'Booster Gold', - 'entity.id': '386', - }, - { - 'entity.lastSeenTimestamp': '2023-02-14T10:24:49.445Z', - 'entity.type': 'host', - 'entity.displayName': 'Plastic Man', - 'entity.id': '387', - }, - { - 'entity.lastSeenTimestamp': '2024-05-10T06:49:45.226Z', - 'entity.type': 'container', - 'entity.displayName': 'Metamorpho', - 'entity.id': '388', - }, - { - 'entity.lastSeenTimestamp': '2023-08-28T11:04:03.884Z', - 'entity.type': 'host', - 'entity.displayName': 'The Spectre', - 'entity.id': '389', - }, - { - 'entity.lastSeenTimestamp': '2023-06-03T09:16:22.294Z', - 'entity.type': 'service', - 'entity.displayName': 'Etrigan', - 'entity.id': '390', - }, - { - 'entity.lastSeenTimestamp': '2023-05-27T15:43:31.368Z', - 'entity.type': 'host', - 'entity.displayName': 'Swamp Thing', - 'entity.id': '391', - }, - { - 'entity.lastSeenTimestamp': '2024-01-23T00:27:36.339Z', - 'entity.type': 'service', - 'entity.displayName': 'Constantine', - 'entity.id': '392', - }, - { - 'entity.lastSeenTimestamp': '2023-12-19T09:00:36.251Z', - 'entity.type': 'host', - 'entity.displayName': 'Zatanna', - 'entity.id': '393', - }, - { - 'entity.lastSeenTimestamp': '2024-02-11T09:31:14.413Z', - 'entity.type': 'host', - 'entity.displayName': 'Doctor Fate', - 'entity.id': '394', - }, - { - 'entity.lastSeenTimestamp': '2024-08-15T14:04:15.345Z', - 'entity.type': 'service', - 'entity.displayName': 'Martian Manhunter', - 'entity.id': '395', - }, - { - 'entity.lastSeenTimestamp': '2024-03-23T06:41:28.527Z', - 'entity.type': 'container', - 'entity.displayName': 'Firestorm', - 'entity.id': '396', - }, - { - 'entity.lastSeenTimestamp': '2023-03-29T19:22:53.314Z', - 'entity.type': 'service', - 'entity.displayName': 'Captain Atom', - 'entity.id': '397', - }, - { - 'entity.lastSeenTimestamp': '2024-05-03T02:22:19.643Z', - 'entity.type': 'service', - 'entity.displayName': 'The Atom', - 'entity.id': '398', - }, - { - 'entity.lastSeenTimestamp': '2024-05-12T05:55:36.153Z', - 'entity.type': 'service', - 'entity.displayName': 'Vixen', - 'entity.id': '399', - }, - { - 'entity.lastSeenTimestamp': '2023-03-01T07:39:44.249Z', - 'entity.type': 'service', - 'entity.displayName': 'Animal Man', - 'entity.id': '400', - }, - { - 'entity.lastSeenTimestamp': '2023-05-20T14:24:33.191Z', - 'entity.type': 'host', - 'entity.displayName': 'Hawk', - 'entity.id': '401', - }, - { - 'entity.lastSeenTimestamp': '2023-06-24T16:44:21.444Z', - 'entity.type': 'host', - 'entity.displayName': 'Dove', - 'entity.id': '402', - }, - { - 'entity.lastSeenTimestamp': '2024-04-05T00:50:29.260Z', - 'entity.type': 'host', - 'entity.displayName': 'Steel', - 'entity.id': '403', - }, - { - 'entity.lastSeenTimestamp': '2024-05-01T07:44:47.694Z', - 'entity.type': 'host', - 'entity.displayName': 'Guardian', - 'entity.id': '404', - }, - { - 'entity.lastSeenTimestamp': '2024-08-10T20:46:37.204Z', - 'entity.type': 'container', - 'entity.displayName': 'The Phantom Stranger', - 'entity.id': '405', - }, - { - 'entity.lastSeenTimestamp': '2024-04-06T11:04:12.556Z', - 'entity.type': 'service', - 'entity.displayName': 'Lobo', - 'entity.id': '406', - }, - { - 'entity.lastSeenTimestamp': '2023-11-24T01:39:36.878Z', - 'entity.type': 'host', - 'entity.displayName': 'Red Tornado', - 'entity.id': '407', - }, - { - 'entity.lastSeenTimestamp': '2024-08-05T14:00:37.985Z', - 'entity.type': 'service', - 'entity.displayName': 'Miss Martian', - 'entity.id': '408', - }, - { - 'entity.lastSeenTimestamp': '2024-01-23T18:57:18.692Z', - 'entity.type': 'container', - 'entity.displayName': 'Bizarro', - 'entity.id': '409', - }, - { - 'entity.lastSeenTimestamp': '2023-01-29T08:35:22.194Z', - 'entity.type': 'service', - 'entity.displayName': 'Black Lightning', - 'entity.id': '410', - }, - { - 'entity.lastSeenTimestamp': '2024-04-03T21:32:10.035Z', - 'entity.type': 'container', - 'entity.displayName': 'Katana', - 'entity.id': '411', - }, - { - 'entity.lastSeenTimestamp': '2024-02-05T09:18:03.386Z', - 'entity.type': 'service', - 'entity.displayName': 'Mr. Terrific', - 'entity.id': '412', - }, - { - 'entity.lastSeenTimestamp': '2024-05-09T01:04:11.713Z', - 'entity.type': 'host', - 'entity.displayName': 'Plastic Man', - 'entity.id': '413', - }, - { - 'entity.lastSeenTimestamp': '2023-03-25T15:26:53.790Z', - 'entity.type': 'host', - 'entity.displayName': 'Shazam', - 'entity.id': '414', - }, - { - 'entity.lastSeenTimestamp': '2023-07-11T11:07:31.377Z', - 'entity.type': 'service', - 'entity.displayName': 'Spawn', - 'entity.id': '415', - }, - { - 'entity.lastSeenTimestamp': '2023-09-08T10:01:26.864Z', - 'entity.type': 'host', - 'entity.displayName': 'Invincible', - 'entity.id': '416', - }, - { - 'entity.lastSeenTimestamp': '2024-07-14T15:51:35.763Z', - 'entity.type': 'container', - 'entity.displayName': 'Atom Eve', - 'entity.id': '417', - }, - { - 'entity.lastSeenTimestamp': '2024-06-26T21:44:30.555Z', - 'entity.type': 'container', - 'entity.displayName': 'Rex Splode', - 'entity.id': '418', - }, - { - 'entity.lastSeenTimestamp': '2023-07-05T04:20:35.073Z', - 'entity.type': 'container', - 'entity.displayName': 'Allen the Alien', - 'entity.id': '419', - }, - { - 'entity.lastSeenTimestamp': '2024-05-31T19:57:53.543Z', - 'entity.type': 'service', - 'entity.displayName': 'Omni-Man', - 'entity.id': '420', - }, - { - 'entity.lastSeenTimestamp': '2023-02-19T17:22:07.379Z', - 'entity.type': 'service', - 'entity.displayName': 'The Tick', - 'entity.id': '421', - }, - { - 'entity.lastSeenTimestamp': '2023-12-17T22:51:04.060Z', - 'entity.type': 'host', - 'entity.displayName': 'Arthur', - 'entity.id': '422', - }, - { - 'entity.lastSeenTimestamp': '2024-03-09T23:54:47.229Z', - 'entity.type': 'service', - 'entity.displayName': 'Big Daddy', - 'entity.id': '423', - }, - { - 'entity.lastSeenTimestamp': '2024-07-14T11:52:37.828Z', - 'entity.type': 'service', - 'entity.displayName': 'Hit-Girl', - 'entity.id': '424', - }, - { - 'entity.lastSeenTimestamp': '2023-02-08T21:15:09.242Z', - 'entity.type': 'container', - 'entity.displayName': 'Kick-Ass', - 'entity.id': '425', - }, - { - 'entity.lastSeenTimestamp': '2024-03-01T17:58:53.274Z', - 'entity.type': 'host', - 'entity.displayName': 'Hellboy', - 'entity.id': '426', - }, - { - 'entity.lastSeenTimestamp': '2023-11-04T20:37:28.218Z', - 'entity.type': 'host', - 'entity.displayName': 'Abe Sapien', - 'entity.id': '427', - }, - { - 'entity.lastSeenTimestamp': '2024-05-16T15:38:01.584Z', - 'entity.type': 'service', - 'entity.displayName': 'Liz Sherman', - 'entity.id': '428', - }, - { - 'entity.lastSeenTimestamp': '2023-03-28T13:40:51.501Z', - 'entity.type': 'container', - 'entity.displayName': 'The Mask', - 'entity.id': '429', - }, - { - 'entity.lastSeenTimestamp': '2023-07-22T10:39:48.045Z', - 'entity.type': 'service', - 'entity.displayName': 'Judge Dredd', - 'entity.id': '430', - }, - { - 'entity.lastSeenTimestamp': '2023-11-10T02:21:09.389Z', - 'entity.type': 'service', - 'entity.displayName': 'Tank Girl', - 'entity.id': '431', - }, - { - 'entity.lastSeenTimestamp': '2024-04-21T16:23:33.730Z', - 'entity.type': 'container', - 'entity.displayName': 'Shadowman', - 'entity.id': '432', - }, - { - 'entity.lastSeenTimestamp': '2023-08-17T19:31:07.282Z', - 'entity.type': 'container', - 'entity.displayName': 'Bloodshot', - 'entity.id': '433', - }, - { - 'entity.lastSeenTimestamp': '2023-04-23T10:05:19.825Z', - 'entity.type': 'service', - 'entity.displayName': 'X-O Manowar', - 'entity.id': '434', - }, - { - 'entity.lastSeenTimestamp': '2024-04-30T21:58:46.410Z', - 'entity.type': 'host', - 'entity.displayName': 'Harbinger', - 'entity.id': '435', - }, - { - 'entity.lastSeenTimestamp': '2023-07-14T05:26:30.493Z', - 'entity.type': 'service', - 'entity.displayName': 'Ninjak', - 'entity.id': '436', - }, - { - 'entity.lastSeenTimestamp': '2024-01-30T09:21:55.939Z', - 'entity.type': 'host', - 'entity.displayName': 'Faith', - 'entity.id': '437', - }, - { - 'entity.lastSeenTimestamp': '2024-02-17T20:36:23.898Z', - 'entity.type': 'host', - 'entity.displayName': 'Archer', - 'entity.id': '438', - }, - { - 'entity.lastSeenTimestamp': '2023-04-04T15:08:08.423Z', - 'entity.type': 'container', - 'entity.displayName': 'Armstrong', - 'entity.id': '439', - }, - { - 'entity.lastSeenTimestamp': '2024-07-29T11:54:01.693Z', - 'entity.type': 'host', - 'entity.displayName': 'Eternal Warrior', - 'entity.id': '440', - }, - { - 'entity.lastSeenTimestamp': '2023-11-02T09:56:15.646Z', - 'entity.type': 'host', - 'entity.displayName': 'Quantum', - 'entity.id': '441', - }, - { - 'entity.lastSeenTimestamp': '2023-04-06T02:07:23.857Z', - 'entity.type': 'container', - 'entity.displayName': 'Woody', - 'entity.id': '442', - }, - { - 'entity.lastSeenTimestamp': '2023-05-20T10:33:26.328Z', - 'entity.type': 'host', - 'entity.displayName': 'The Darkness', - 'entity.id': '443', - }, - { - 'entity.lastSeenTimestamp': '2023-12-03T23:59:21.627Z', - 'entity.type': 'container', - 'entity.displayName': 'Witchblade', - 'entity.id': '444', - }, - { - 'entity.lastSeenTimestamp': '2023-05-31T10:56:01.829Z', - 'entity.type': 'container', - 'entity.displayName': 'Ripclaw', - 'entity.id': '445', - }, - { - 'entity.lastSeenTimestamp': '2024-07-28T11:56:20.407Z', - 'entity.type': 'host', - 'entity.displayName': 'Warblade', - 'entity.id': '446', - }, - { - 'entity.lastSeenTimestamp': '2023-05-03T18:24:08.227Z', - 'entity.type': 'host', - 'entity.displayName': 'Savage Dragon', - 'entity.id': '447', - }, - { - 'entity.lastSeenTimestamp': '2024-07-15T09:05:19.621Z', - 'entity.type': 'host', - 'entity.displayName': 'Spawn', - 'entity.id': '448', - }, - { - 'entity.lastSeenTimestamp': '2024-04-16T13:06:48.941Z', - 'entity.type': 'host', - 'entity.displayName': 'Witchblade', - 'entity.id': '449', - }, - { - 'entity.lastSeenTimestamp': '2024-04-22T12:52:06.912Z', - 'entity.type': 'container', - 'entity.displayName': 'Invincible', - 'entity.id': '450', - }, - { - 'entity.lastSeenTimestamp': '2023-02-23T23:57:49.389Z', - 'entity.type': 'host', - 'entity.displayName': 'The Maxx', - 'entity.id': '451', - }, - { - 'entity.lastSeenTimestamp': '2024-04-17T01:12:16.359Z', - 'entity.type': 'service', - 'entity.displayName': 'Lady Death', - 'entity.id': '452', - }, - { - 'entity.lastSeenTimestamp': '2024-05-07T14:14:02.286Z', - 'entity.type': 'container', - 'entity.displayName': 'The Shadow', - 'entity.id': '453', - }, - { - 'entity.lastSeenTimestamp': '2024-03-31T23:20:56.580Z', - 'entity.type': 'host', - 'entity.displayName': 'Doc Savage', - 'entity.id': '454', - }, - { - 'entity.lastSeenTimestamp': '2023-05-18T01:28:20.743Z', - 'entity.type': 'container', - 'entity.displayName': 'Zorro', - 'entity.id': '455', - }, - { - 'entity.lastSeenTimestamp': '2023-01-12T01:19:03.220Z', - 'entity.type': 'service', - 'entity.displayName': 'The Phantom', - 'entity.id': '456', - }, - { - 'entity.lastSeenTimestamp': '2023-10-10T20:35:47.302Z', - 'entity.type': 'container', - 'entity.displayName': 'Green Hornet', - 'entity.id': '457', - }, - { - 'entity.lastSeenTimestamp': '2023-05-09T19:35:59.568Z', - 'entity.type': 'service', - 'entity.displayName': 'Kato', - 'entity.id': '458', - }, - { - 'entity.lastSeenTimestamp': '2023-07-02T19:40:18.206Z', - 'entity.type': 'host', - 'entity.displayName': 'Red Sonja', - 'entity.id': '459', - }, - { - 'entity.lastSeenTimestamp': '2024-01-08T20:03:24.184Z', - 'entity.type': 'container', - 'entity.displayName': 'Conan the Barbarian', - 'entity.id': '460', - }, - { - 'entity.lastSeenTimestamp': '2024-03-13T05:26:16.730Z', - 'entity.type': 'service', - 'entity.displayName': 'Homer Simpson', - 'entity.id': '461', - }, - { - 'entity.lastSeenTimestamp': '2024-06-28T02:49:37.987Z', - 'entity.type': 'host', - 'entity.displayName': 'Marge Simpson', - 'entity.id': '462', - }, - { - 'entity.lastSeenTimestamp': '2024-06-17T21:16:08.180Z', - 'entity.type': 'host', - 'entity.displayName': 'Bart Simpson', - 'entity.id': '463', - }, - { - 'entity.lastSeenTimestamp': '2023-03-27T21:34:38.051Z', - 'entity.type': 'host', - 'entity.displayName': 'Lisa Simpson', - 'entity.id': '464', - }, - { - 'entity.lastSeenTimestamp': '2023-02-04T21:08:36.340Z', - 'entity.type': 'service', - 'entity.displayName': 'Maggie Simpson', - 'entity.id': '465', - }, - { - 'entity.lastSeenTimestamp': '2024-05-22T20:05:45.805Z', - 'entity.type': 'service', - 'entity.displayName': 'Abe Simpson', - 'entity.id': '466', - }, - { - 'entity.lastSeenTimestamp': '2023-04-02T23:57:33.378Z', - 'entity.type': 'container', - 'entity.displayName': 'Ned Flanders', - 'entity.id': '467', - }, - { - 'entity.lastSeenTimestamp': '2023-03-05T12:25:19.985Z', - 'entity.type': 'container', - 'entity.displayName': 'Maude Flanders', - 'entity.id': '468', - }, - { - 'entity.lastSeenTimestamp': '2024-05-31T22:44:52.035Z', - 'entity.type': 'container', - 'entity.displayName': 'Rod Flanders', - 'entity.id': '469', - }, - { - 'entity.lastSeenTimestamp': '2024-03-06T22:07:45.916Z', - 'entity.type': 'container', - 'entity.displayName': 'Todd Flanders', - 'entity.id': '470', - }, - { - 'entity.lastSeenTimestamp': '2023-09-29T20:39:30.536Z', - 'entity.type': 'service', - 'entity.displayName': 'Milhouse Van Houten', - 'entity.id': '471', - }, - { - 'entity.lastSeenTimestamp': '2023-07-13T22:08:03.669Z', - 'entity.type': 'host', - 'entity.displayName': 'Nelson Muntz', - 'entity.id': '472', - }, - { - 'entity.lastSeenTimestamp': '2024-01-11T11:44:27.608Z', - 'entity.type': 'service', - 'entity.displayName': 'Ralph Wiggum', - 'entity.id': '473', - }, - { - 'entity.lastSeenTimestamp': '2023-10-07T03:48:20.334Z', - 'entity.type': 'container', - 'entity.displayName': 'Chief Wiggum', - 'entity.id': '474', - }, - { - 'entity.lastSeenTimestamp': '2023-12-26T00:46:10.602Z', - 'entity.type': 'host', - 'entity.displayName': 'Clancy Wiggum', - 'entity.id': '475', - }, - { - 'entity.lastSeenTimestamp': '2023-03-24T03:32:51.643Z', - 'entity.type': 'host', - 'entity.displayName': 'Krusty the Clown', - 'entity.id': '476', - }, - { - 'entity.lastSeenTimestamp': '2023-01-19T18:15:10.942Z', - 'entity.type': 'container', - 'entity.displayName': 'Sideshow Bob', - 'entity.id': '477', - }, - { - 'entity.lastSeenTimestamp': '2023-02-05T23:13:30.639Z', - 'entity.type': 'service', - 'entity.displayName': 'Sideshow Mel', - 'entity.id': '478', - }, - { - 'entity.lastSeenTimestamp': '2024-03-06T07:02:19.760Z', - 'entity.type': 'host', - 'entity.displayName': 'Moe Szyslak', - 'entity.id': '479', - }, - { - 'entity.lastSeenTimestamp': '2024-08-26T17:28:47.162Z', - 'entity.type': 'service', - 'entity.displayName': 'Barney Gumble', - 'entity.id': '480', - }, - { - 'entity.lastSeenTimestamp': '2024-05-12T12:10:32.668Z', - 'entity.type': 'service', - 'entity.displayName': 'Lenny Leonard', - 'entity.id': '481', - }, - { - 'entity.lastSeenTimestamp': '2023-07-25T05:19:12.244Z', - 'entity.type': 'service', - 'entity.displayName': 'Carl Carlson', - 'entity.id': '482', - }, - { - 'entity.lastSeenTimestamp': '2023-09-14T19:23:00.311Z', - 'entity.type': 'container', - 'entity.displayName': 'Waylon Smithers', - 'entity.id': '483', - }, - { - 'entity.lastSeenTimestamp': '2023-07-06T12:21:13.655Z', - 'entity.type': 'service', - 'entity.displayName': 'Mr. Burns', - 'entity.id': '484', - }, - { - 'entity.lastSeenTimestamp': '2023-01-23T07:14:22.901Z', - 'entity.type': 'service', - 'entity.displayName': 'Principal Skinner', - 'entity.id': '485', - }, - { - 'entity.lastSeenTimestamp': '2024-05-07T18:03:19.312Z', - 'entity.type': 'service', - 'entity.displayName': 'Edna Krabappel', - 'entity.id': '486', - }, - { - 'entity.lastSeenTimestamp': '2023-02-14T07:33:02.981Z', - 'entity.type': 'service', - 'entity.displayName': 'Superintendent Chalmers', - 'entity.id': '487', - }, - { - 'entity.lastSeenTimestamp': '2024-01-21T22:32:55.738Z', - 'entity.type': 'container', - 'entity.displayName': 'Groundskeeper Willie', - 'entity.id': '488', - }, - { - 'entity.lastSeenTimestamp': '2024-03-31T13:42:07.765Z', - 'entity.type': 'service', - 'entity.displayName': 'Otto Mann', - 'entity.id': '489', - }, - { - 'entity.lastSeenTimestamp': '2023-08-23T18:26:32.084Z', - 'entity.type': 'container', - 'entity.displayName': 'Apu Nahasapeemapetilon', - 'entity.id': '490', - }, - { - 'entity.lastSeenTimestamp': '2024-02-14T04:17:17.737Z', - 'entity.type': 'container', - 'entity.displayName': 'Manjula Nahasapeemapetilon', - 'entity.id': '491', - }, - { - 'entity.lastSeenTimestamp': '2024-07-06T03:25:46.939Z', - 'entity.type': 'service', - 'entity.displayName': 'Kearney Zzyzwicz', - 'entity.id': '492', - }, - { - 'entity.lastSeenTimestamp': '2023-09-04T06:08:42.239Z', - 'entity.type': 'service', - 'entity.displayName': 'Jimbo Jones', - 'entity.id': '493', - }, - { - 'entity.lastSeenTimestamp': '2023-06-12T23:45:21.630Z', - 'entity.type': 'host', - 'entity.displayName': 'Dolph Starbeam', - 'entity.id': '494', - }, - { - 'entity.lastSeenTimestamp': '2023-11-18T18:43:41.585Z', - 'entity.type': 'container', - 'entity.displayName': 'Martin Prince', - 'entity.id': '495', - }, - { - 'entity.lastSeenTimestamp': '2024-07-29T01:12:36.480Z', - 'entity.type': 'container', - 'entity.displayName': 'Mrs. Prince', - 'entity.id': '496', - }, - { - 'entity.lastSeenTimestamp': '2023-09-25T18:32:05.791Z', - 'entity.type': 'container', - 'entity.displayName': 'Comic Book Guy', - 'entity.id': '497', - }, - { - 'entity.lastSeenTimestamp': '2023-04-05T12:49:08.814Z', - 'entity.type': 'host', - 'entity.displayName': 'Professor Frink', - 'entity.id': '498', - }, - { - 'entity.lastSeenTimestamp': '2023-04-07T20:07:02.744Z', - 'entity.type': 'host', - 'entity.displayName': 'Troy McClure', - 'entity.id': '499', - }, -] as unknown as InventoryEntitiesAPIReturnType['entities']; +]; + +const hostsMock = Array.from({ length: 20 }, () => getEntity('host')); +const containersMock = Array.from({ length: 20 }, () => getEntity('container')); +const servicesMock = Array.from({ length: 20 }, () => getEntity('service')); + +export const entitiesMock = [ + ...alertsMock, + ...hostsMock, + ...containersMock, + ...servicesMock, +] as Entity[]; diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index c4c8f5d3ac59d..67de9919c6324 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -51,6 +51,7 @@ "@kbn/observability-plugin", "@kbn/rule-data-utils", "@kbn/spaces-plugin", - "@kbn/cloud-plugin" + "@kbn/cloud-plugin", + "@kbn/storybook" ] } From 300678ca85209159c9c2cbb5a92c0b49dc0984d7 Mon Sep 17 00:00:00 2001 From: Elena Shostak <165678770+elena-shostak@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:30:53 +0200 Subject: [PATCH 02/12] [Docs] Security Route Configuration (#193994) ## Summary Added documentation for the security route configuration. ### Checklist - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials --------- Co-authored-by: Elastic Machine --- dev_docs/key_concepts/api_authorization.mdx | 319 ++++++++++++++++++ .../{security.mdx => kibana_system_user.mdx} | 38 +-- dev_docs/nav-kibana-dev.docnav.json | 7 +- 3 files changed, 329 insertions(+), 35 deletions(-) create mode 100644 dev_docs/key_concepts/api_authorization.mdx rename dev_docs/key_concepts/{security.mdx => kibana_system_user.mdx} (62%) diff --git a/dev_docs/key_concepts/api_authorization.mdx b/dev_docs/key_concepts/api_authorization.mdx new file mode 100644 index 0000000000000..b781808757c9a --- /dev/null +++ b/dev_docs/key_concepts/api_authorization.mdx @@ -0,0 +1,319 @@ +--- +id: kibDevDocsSecurityAPIAuthorization +slug: /kibana-dev-docs/key-concepts/security-api-authorization +title: Kibana API authorization +description: This guide provides an overview of API authorization in Kibana. +date: 2024-10-04 +tags: ['kibana', 'dev', 'contributor', 'security'] +--- + +Authorization is an important aspect of API design. It must be considered for all endpoints, even those marked as `internal`. This guide explains how and when to apply authorization to your endpoints + +Table of contents: +1. [API authorization](#api-authorization) +2. [[Deprecated] Adding API authorization with `access` tags](#deprecated-adding-api-authorization-with-access-tags) + - [Why not add `access` tags to all routes by default?](#why-not-add-access-tags-to-all-routes-by-default) +3. [Adding API authorization with `security` configuration](#adding-api-authorization-with-security-configuration) + - [Key features](#key-features) + - [Configuring authorization on routes](#configuring-authorization-on-routes) + - [Opting out of authorization for specific routes](#opting-out-of-authorization-for-specific-routes) + - [Classic router security configuration examples](#classic-router-security-configuration-examples) + - [Versioned router security configuration examples](#versioned-router-security-configuration-examples) +4. [Authorization response available in route handlers](#authorization-response-available-in-route-handlers) +5. [OpenAPI specification (OAS) documentation](#openapi-specification-oas-documentation) +6. [Migrating from `access` tags to `security` configuration](#migrating-from-access-tags-to-security-configuration) +7. [Questions?](#questions) + +## API authorization +Kibana API routes do not have any authorization checks applied by default. This means that your APIs are accessible to anyone with valid credentials, regardless of their permissions. This includes users with no roles assigned. +This on its own is insufficient, and care must be taken to ensure that only authorized users can invoke your endpoints. + +Kibana leverages for a majority of its persistence. The Saved Objects Service performs its own authorization checks, so if your API route is primarily a CRUD interface to Saved Objects, then your authorization needs are likely already met. +This is also true for derivatives of the Saved Objects Service, such as the Alerting and Cases services. + +If your endpoint is not a CRUD interface to Saved Objects, or if your endpoint bypasses our built-in Saved Objects authorization checks, then you must ensure that only authorized users can invoke your endpoint. +This is **especially** important if your route does any of the following: +1. Performs non-insignificant processing, causing load on the Elasticsearch cluster or the Kibana server. +2. Calls Elasticsearch APIs using the internal `kibana_system` user. +3. Calls a third-party service. +4. Exposes any non-public information to the caller, such as system configuration or state, as part of the successful or even error response. + +## [Deprecated] Adding API authorization with `access` tags +**Note**: `access` tags were deprecated in favour of `security` configuration. + +`access` tags are used to restrict access to API routes. They are used to ensure that only users with the required privileges can access the route. + +Example configuration: +```ts +router.get({ + path: '/api/path', + options: { + tags: ['access:', 'access:'], + }, + ... +}, handler); +``` + +More information on adding `access` tags to your routes can be found temporarily in the [legacy documentation](https://www.elastic.co/guide/en/kibana/current/development-security.html#development-plugin-feature-registration) + +### Why not add `access` tags to all routes by default? +Each authorization check that we perform involves a round-trip to Elasticsearch, so they are not as cheap as we'd like. Therefore, we want to keep the number of authorization checks we perform within a single route to a minimum. +Adding an `access` tag to routes that leverage the Saved Objects Service would be redundant in most cases, since the Saved Objects Service will be performing authorization anyway. + + +## Adding API authorization with `security` configuration +`KibanaRouteOptions` provides a security configuration at the route definition level, offering robust security configurations for both **Classic** and **Versioned** routes. + +### Key features: +1. **Fine-grained control**: + - Define the exact privileges required to access the route. + - Use `requiredPrivileges` to specify privileges with support for complex rules: + - **AND rules** using `allRequired`: Requires all specified privileges for access. + - **OR rules** using `anyRequired`: Allows access if any one of the specified privileges is met. + - **Complex Nested Rules**: Combine both `allRequired` and `anyRequired` for advanced access rules. +2. **Explicit Opt-out**: Provide a reason for opting out of authorization to maintain transparency. +3. **Versioned Routes**: Define security configurations for different versions of the same route. +4. **Improved Documentation with OpenAPI (OAS)**: Automatically generated OAS documentation with the required privileges for each route. +5. **AuthzResult Object in Route Handlers**: Access the authorization response in route handlers to see which privileges were met. + + +### Configuring authorization on routes +**Before migration:** +```ts +router.get({ + path: '/api/path', + options: { + tags: ['access:', 'access:'], + }, + ... +}, handler); +``` + +**After migration:** +```ts +router.get({ + path: '/api/path', + security: { + authz: { + requiredPrivileges: ['', ''], + }, + }, + ... +}, handler); +``` + +### Opting out of authorization for specific routes +**Before migration:** +```ts +router.get({ + path: '/api/path', + ... +}, handler); +``` + +**After migration:** +```ts +router.get({ + path: '/api/path', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization because ...', + }, + }, + ... +}, handler); +``` + +### Classic router security configuration examples + +**Example 1: All privileges required.** +Requires `` AND `` to access the route. +```ts +router.get({ + path: '/api/path', + security: { + authz: { + requiredPrivileges: ['', ''], + }, + }, + ... +}, handler); +``` + +**Example 2: Any privileges required.** +Requires `` OR `` to access the route. +```ts +router.get({ + path: '/api/path', + security: { + authz: { + requiredPrivileges: [{ anyRequired: ['', ''] }], + }, + }, + ... +}, handler); +``` + +**Example 3: Complex configuration.** +Requires `` AND `` AND (`` OR ``) to access the route. +```ts +router.get({ + path: '/api/path', + security: { + authz: { + requiredPrivileges: [{ allRequired: ['', ''], anyRequired: ['', ''] }], + }, + }, + ... +}, handler); +``` + +### Versioned router security configuration examples +Different security configurations can be applied to each version when using the Versioned Router. This allows your authorization needs to evolve in lockstep with your API. + +**Example 1: Default and custom version security.** + +1. **Default configuration**: Applies to versions without specific authorization, requires ``. + +2. **Version 1**: Requires **both** `` and `` privileges. + +3. **Version 2**: Inherits the default authorization configuration, requiring ``. + +```ts +router.versioned + .get({ + path: '/internal/path', + access: 'internal', + // default security configuration, will be used for version unless overridden + security: { + authz: { + requiredPrivileges: [''], + }, + }, + }) + .addVersion({ + version: '1', + validate: false, + security: { + authz: { + requiredPrivileges: ['', ''], + }, + }, + }, handlerV1) + .addVersion({ + version: '2', + validate: false, + }, handlerV2); +``` + +**Example 2: Multiple versions with different security requirements.** +1. **Default Configuration**: Applies to versions without specific authorization, requires ``. + +2. **Version 1**: Requires **both** `` and `` privileges. + +3. **Version 2**: Requires `` AND (`` OR ``). + +4. **Version 3**: Requires only ``. + +```ts +router.versioned + .get({ + path: '/internal/path', + access: 'internal', + // default security configuration, will be used for version unless overridden + security: { + authz: { + requiredPrivileges: [''], + }, + }, + }) + .addVersion({ + version: '1', + validate: false, + security: { + authz: { + requiredPrivileges: ['', ''], + }, + }, + }, handlerV1) + .addVersion({ + version: '2', + validate: false, + security: { + authz: { + requiredPrivileges: ['', anyRequired: ['', '']], + }, + }, + }, handlerV2) + .addVersion({ + version: '3', + validate: false, + security: { + authz: { + requiredPrivileges: [''], + }, + }, + }, handlerV3); +``` + +## Authorization response available in route handlers +The `AuthzResult` object is available in route handlers, which provides information about the privileges granted to the caller. +For example, you have a route that requires `` and ANY of the privileges `` OR ``: +```ts +router.get({ + path: '/api/path', + security: { + authz: { + requiredPrivileges: ['', { anyRequired: ['', ''] }], + }, + }, + ... +}, (context, request, response) => { + // The authorization response is available in `request.authzResult` + // { + // "": true, + // "": true, + // "": false + // } +}); +``` + +## OpenAPI specification (OAS) documentation +Based on the security configuration defined in routes, OAS documentation will automatically generate and include description about the required privileges. +This makes it easy to view the security requirements of each endpoint in a standardized format, facilitating better understanding and usage by developers or teams consuming the API. + +To check the OAS documentation for a specific API route and see its security details, you can use the following command: +```sh +GET /api/oas?pathStartsWith=/your/api/path +``` + +## Migrating from `access` tags to `security` configuration +We aim to use the same privileges that are currently defined with tags `access:`. +To assist with this migration, we have created eslint rule `no_deprecated_authz_config`, that will automatically convert your `access` tags to the new `security` configuration. +It scans route definitions and converts `access` tags to the new `requiredPriviliges` configuration. + +Note: The rule is disabled by default to avoid automatic migrations without an oversight. You can perform migrations by running: + +**Migrate routes with defined authorization** +```sh +MIGRATE_DISABLED_AUTHZ=false MIGRATE_ENABLED_AUTHZ=true npx eslint --ext .ts --fix path/to/your/folder +``` + +**Migrate routes opted out from authorization** +```sh +MIGRATE_DISABLED_AUTHZ=true MIGRATE_ENABLED_AUTHZ=false npx eslint --ext .ts --fix path/to/your/folder +``` +We encourage you to migrate routes that are opted out from authorization to new config and provide legitimate reason for disabled authorization. +It is better to migrate routes opted out from authorization iteratively and elaborate on the reasoning. +Routes without a compelling reason to opt-out of authorization should plan to introduce them as soon as possible. + +**Migrate all routes** +```sh +MIGRATE_DISABLED_AUTHZ=true MIGRATE_ENABLED_AUTHZ=true npx eslint --ext .ts --fix path/to/your/folder +``` + +## Questions? +If you have any questions or need help with API authorization, please reach out to the `@elastic/kibana-security` team. + + diff --git a/dev_docs/key_concepts/security.mdx b/dev_docs/key_concepts/kibana_system_user.mdx similarity index 62% rename from dev_docs/key_concepts/security.mdx rename to dev_docs/key_concepts/kibana_system_user.mdx index 8e0bed133fe79..0373c8fa5d402 100644 --- a/dev_docs/key_concepts/security.mdx +++ b/dev_docs/key_concepts/kibana_system_user.mdx @@ -1,40 +1,12 @@ --- -id: kibDevDocsSecurityIntro -slug: /kibana-dev-docs/key-concepts/security-intro -title: Security -description: Maintaining Kibana's security posture -date: 2023-07-11 +id: kibDevDocsSecurityKibanaSystemUser +slug: /kibana-dev-docs/key-concepts/security-kibana-system-user +title: Security Kibana System User +description: This guide provides an overview of `kibana_system` user +date: 2024-10-04 tags: ['kibana', 'dev', 'contributor', 'security'] --- -Security is everyone's responsibility. This is inclusive of design, product, and engineering. The purpose of this guide is to give a high-level overview of security constructs and expectations. - -This guide covers the following topics: - -* [API authorization](#api-authorization) -* [The `kibana_system` user](#the-kibana_system-user) - -## API authorization -Kibana API routes do not have any authorization checks applied by default. This means that your APIs are accessible to anyone with valid credentials, regardless of their permissions. This includes users with no roles assigned. -This on its own is insufficient, and care must be taken to ensure that only authorized users can invoke your endpoints. - -### Adding API authorization -Kibana leverages for a majority of its persistence. The Saved Objects Service performs its own authorization checks, so if your API route is primarily a CRUD interface to Saved Objects, then your authorization needs are already met. -This is also true for derivatives of the Saved Objects Service, such as the Alerting and Cases services. - -If your endpoint is not a CRUD interface to Saved Objects, then your route should include `access` tags to ensure that only authorized users can invoke your endpoint. This is **especially** important if your route does any of the following: -1. Performs non-insignificant processing, causing load on the Kibana server. -2. Calls Elasticsearch using the internal `kibana_system` user. -3. Calls a third-party service. -4. Returns any non-public information to the caller, such as system configuration or state. - -More information on adding `access` tags to your routes can be found temporarily in the [legacy documentation](https://www.elastic.co/guide/en/kibana/current/development-security.html#development-plugin-feature-registration) - -### Why not add `access` tags to all routes by default? -Each authorization check that we perform involves a round-trip to Elasticsearch, so they are not as cheap as we'd like. Therefore, we want to keep the number of authorization checks we perform within a single route to a minimum. -Adding an `access` tag to routes that leverage the Saved Objects Service would be redundant in most cases, since the Saved Objects Service will be performing authorization anyway. - - ## The `kibana_system` user The Kibana server authenticates to Elasticsearch using the `elastic/kibana` [service account](https://www.elastic.co/guide/en/elasticsearch/reference/current/service-accounts.html#service-accounts-explanation). This service account has privileges that are equivilent to the `kibana_system` reserved role, whose descriptor is managed in the Elasticsearch repository ([source link](https://github.com/elastic/elasticsearch/blob/430cde6909eae12e1a90ac2bff29b71cbf4af18b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/KibanaOwnedReservedRoleDescriptors.java#L58)). diff --git a/dev_docs/nav-kibana-dev.docnav.json b/dev_docs/nav-kibana-dev.docnav.json index a7d696fc10574..6dd2ca052b7bd 100644 --- a/dev_docs/nav-kibana-dev.docnav.json +++ b/dev_docs/nav-kibana-dev.docnav.json @@ -101,7 +101,10 @@ "id": "kibBuildingBlocks" }, { - "id": "kibDevDocsSecurityIntro" + "id": "kibDevDocsSecurityAPIAuthorization" + }, + { + "id": "kibDevDocsSecurityKibanaSystemUser" }, { "id": "kibDevFeaturePrivileges", @@ -653,4 +656,4 @@ ] } ] -} \ No newline at end of file +} From 2a085e103afe8c7bdfb626d0dc683fc8be0e6c05 Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Tue, 22 Oct 2024 13:34:19 +0200 Subject: [PATCH 03/12] Add ESLINT constraints to detect inter-group dependencies (#194810) ## Summary Addresses https://github.com/elastic/kibana-team/issues/1175 As part of the **Sustainable Kibana Architecture** initiative, this PR sets the foundation to start classifying plugins in isolated groups, matching our current solutions / project types: * It adds support for the following fields in the packages' manifests (kibana.jsonc): * `group?: 'search' | 'security' | 'observability' | 'platform' | 'common'` * `visibility?: 'private' | 'shared'` * It proposes a folder structure to automatically infer groups: ```javascript 'src/platform/plugins/shared': { group: 'platform', visibility: 'shared', }, 'src/platform/plugins/internal': { group: 'platform', visibility: 'private', }, 'x-pack/platform/plugins/shared': { group: 'platform', visibility: 'shared', }, 'x-pack/platform/plugins/internal': { group: 'platform', visibility: 'private', }, 'x-pack/solutions/observability/plugins': { group: 'observability', visibility: 'private', }, 'x-pack/solutions/security/plugins': { group: 'security', visibility: 'private', }, 'x-pack/solutions/search/plugins': { group: 'search', visibility: 'private', }, ``` * If a plugin is moved to one of the specific locations above, the group and visibility in the manifest (if specified) must match those inferred from the path. * Plugins that are not relocated are considered: `group: 'common', visibility: 'shared'` by default. As soon as we specify a custom `group`, the ESLINT rules will check violations against dependencies / dependants. The ESLINT rules are pretty simple: * Plugins can only depend on: * Plugins in the same group * OR plugins with `'shared'` visibility * Plugins in `'observability', 'security', 'search'` groups are mandatorily `'private'`. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 7 +- package.json | 1 + .../src/plugin_list/run_plugin_list_cli.ts | 35 ++- packages/kbn-eslint-config/.eslintrc.js | 5 +- .../src/helpers/protected_rules.ts | 2 + packages/kbn-eslint-plugin-imports/index.ts | 4 + .../src/helpers/groups.ts | 25 ++ .../src/helpers/report.ts | 16 + .../src/rules/no_boundary_crossing.test.ts | 16 +- .../src/rules/no_boundary_crossing.ts | 23 +- .../rules/no_group_crossing_imports.test.ts | 155 ++++++++++ .../src/rules/no_group_crossing_imports.ts | 77 +++++ .../rules/no_group_crossing_manifests.test.ts | 280 ++++++++++++++++++ .../src/rules/no_group_crossing_manifests.ts | 158 ++++++++++ .../kbn-eslint-plugin-imports/tsconfig.json | 1 + .../src/commands/codeowners_command.ts | 6 +- .../src/kibana_json_v2_schema.ts | 14 + packages/kbn-manifest/README.md | 30 ++ packages/kbn-manifest/index.ts | 46 +++ packages/kbn-manifest/jest.config.js | 14 + packages/kbn-manifest/kibana.jsonc | 5 + packages/kbn-manifest/manifest.ts | 113 +++++++ packages/kbn-manifest/package.json | 6 + packages/kbn-manifest/tsconfig.json | 23 ++ packages/kbn-repo-info/types.ts | 5 + packages/kbn-repo-packages/modern/package.js | 69 +++++ .../modern/parse_package_manifest.js | 33 ++- packages/kbn-repo-packages/modern/types.ts | 11 +- packages/kbn-repo-packages/tsconfig.json | 3 + .../kbn-repo-source-classifier/src/group.ts | 63 ++++ .../src/module_id.ts | 12 +- .../src/repo_source_classifier.ts | 51 +++- .../kbn-repo-source-classifier/tsconfig.json | 1 + scripts/manifest.js | 11 + tsconfig.base.json | 2 + yarn.lock | 4 + 36 files changed, 1278 insertions(+), 49 deletions(-) create mode 100644 packages/kbn-eslint-plugin-imports/src/helpers/groups.ts create mode 100644 packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.test.ts create mode 100644 packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.ts create mode 100644 packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.test.ts create mode 100644 packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.ts create mode 100644 packages/kbn-manifest/README.md create mode 100644 packages/kbn-manifest/index.ts create mode 100644 packages/kbn-manifest/jest.config.js create mode 100644 packages/kbn-manifest/kibana.jsonc create mode 100644 packages/kbn-manifest/manifest.ts create mode 100644 packages/kbn-manifest/package.json create mode 100644 packages/kbn-manifest/tsconfig.json create mode 100644 packages/kbn-repo-source-classifier/src/group.ts create mode 100644 scripts/manifest.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 56fe95cd65b39..c000628cf9c52 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -597,6 +597,7 @@ packages/kbn-management/settings/types @elastic/kibana-management packages/kbn-management/settings/utilities @elastic/kibana-management packages/kbn-management/storybook/config @elastic/kibana-management test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management +packages/kbn-manifest @elastic/kibana-core packages/kbn-mapbox-gl @elastic/kibana-presentation x-pack/examples/third_party_maps_source_example @elastic/kibana-presentation src/plugins/maps_ems @elastic/kibana-presentation @@ -929,9 +930,9 @@ packages/kbn-test-eui-helpers @elastic/kibana-visualizations x-pack/test/licensing_plugin/plugins/test_feature_usage @elastic/kibana-security packages/kbn-test-jest-helpers @elastic/kibana-operations @elastic/appex-qa packages/kbn-test-subj-selector @elastic/kibana-operations @elastic/appex-qa -x-pack/test_serverless -test -x-pack/test +x-pack/test_serverless +test +x-pack/test x-pack/performance @elastic/appex-qa x-pack/examples/testing_embedded_lens @elastic/kibana-visualizations x-pack/examples/third_party_lens_navigation_prompt @elastic/kibana-visualizations diff --git a/package.json b/package.json index 003047638d9d0..51d1b7472c6dc 100644 --- a/package.json +++ b/package.json @@ -635,6 +635,7 @@ "@kbn/management-settings-types": "link:packages/kbn-management/settings/types", "@kbn/management-settings-utilities": "link:packages/kbn-management/settings/utilities", "@kbn/management-test-plugin": "link:test/plugin_functional/plugins/management_test_plugin", + "@kbn/manifest": "link:packages/kbn-manifest", "@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl", "@kbn/maps-custom-raster-source-plugin": "link:x-pack/examples/third_party_maps_source_example", "@kbn/maps-ems-plugin": "link:src/plugins/maps_ems", diff --git a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts index a201cfcd0e262..65f6735e22ca6 100644 --- a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts +++ b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts @@ -20,14 +20,39 @@ const OUTPUT_PATH = Path.resolve(REPO_ROOT, 'docs/developer/plugin-list.asciidoc export function runPluginListCli() { run(async ({ log }) => { log.info('looking for oss plugins'); - const ossPlugins = discoverPlugins('src/plugins'); - log.success(`found ${ossPlugins.length} plugins`); + const ossLegacyPlugins = discoverPlugins('src/plugins'); + const ossPlatformPlugins = discoverPlugins('src/platform/plugins'); + log.success(`found ${ossLegacyPlugins.length + ossPlatformPlugins.length} plugins`); log.info('looking for x-pack plugins'); - const xpackPlugins = discoverPlugins('x-pack/plugins'); - log.success(`found ${xpackPlugins.length} plugins`); + const xpackLegacyPlugins = discoverPlugins('x-pack/plugins'); + const xpackPlatformPlugins = discoverPlugins('x-pack/platform/plugins'); + const xpackSearchPlugins = discoverPlugins('x-pack/solutions/search/plugins'); + const xpackSecurityPlugins = discoverPlugins('x-pack/solutions/security/plugins'); + const xpackObservabilityPlugins = discoverPlugins('x-pack/solutions/observability/plugins'); + log.success( + `found ${ + xpackLegacyPlugins.length + + xpackPlatformPlugins.length + + xpackSearchPlugins.length + + xpackSecurityPlugins.length + + xpackObservabilityPlugins.length + } plugins` + ); log.info('writing plugin list to', OUTPUT_PATH); - Fs.writeFileSync(OUTPUT_PATH, generatePluginList(ossPlugins, xpackPlugins)); + Fs.writeFileSync( + OUTPUT_PATH, + generatePluginList( + [...ossLegacyPlugins, ...ossPlatformPlugins], + [ + ...xpackLegacyPlugins, + ...xpackPlatformPlugins, + ...xpackSearchPlugins, + ...xpackSecurityPlugins, + ...xpackObservabilityPlugins, + ] + ) + ); }); } diff --git a/packages/kbn-eslint-config/.eslintrc.js b/packages/kbn-eslint-config/.eslintrc.js index 4c429d3157fd9..ec39d88606438 100644 --- a/packages/kbn-eslint-config/.eslintrc.js +++ b/packages/kbn-eslint-config/.eslintrc.js @@ -317,6 +317,7 @@ module.exports = { '@kbn/disable/no_naked_eslint_disable': 'error', '@kbn/eslint/no_async_promise_body': 'error', '@kbn/eslint/no_async_foreach': 'error', + '@kbn/eslint/no_deprecated_authz_config': 'error', '@kbn/eslint/no_trailing_import_slash': 'error', '@kbn/eslint/no_constructor_args_in_property_initializers': 'error', '@kbn/eslint/no_this_in_property_initializers': 'error', @@ -326,8 +327,8 @@ module.exports = { '@kbn/imports/uniform_imports': 'error', '@kbn/imports/no_unused_imports': 'error', '@kbn/imports/no_boundary_crossing': 'error', - '@kbn/eslint/no_deprecated_authz_config': 'error', - + '@kbn/imports/no_group_crossing_manifests': 'error', + '@kbn/imports/no_group_crossing_imports': 'error', 'no-new-func': 'error', 'no-implied-eval': 'error', 'no-prototype-builtins': 'error', diff --git a/packages/kbn-eslint-plugin-disable/src/helpers/protected_rules.ts b/packages/kbn-eslint-plugin-disable/src/helpers/protected_rules.ts index 0eabafc48ab69..6e555f1d9527c 100644 --- a/packages/kbn-eslint-plugin-disable/src/helpers/protected_rules.ts +++ b/packages/kbn-eslint-plugin-disable/src/helpers/protected_rules.ts @@ -12,4 +12,6 @@ export const PROTECTED_RULES = new Set([ '@kbn/disable/no_protected_eslint_disable', '@kbn/disable/no_naked_eslint_disable', '@kbn/imports/no_unused_imports', + '@kbn/imports/no_group_crossing_imports', + '@kbn/imports/no_group_crossing_manifests', ]); diff --git a/packages/kbn-eslint-plugin-imports/index.ts b/packages/kbn-eslint-plugin-imports/index.ts index 9c57d66f60225..31e3483ea6139 100644 --- a/packages/kbn-eslint-plugin-imports/index.ts +++ b/packages/kbn-eslint-plugin-imports/index.ts @@ -13,6 +13,8 @@ import { UniformImportsRule } from './src/rules/uniform_imports'; import { ExportsMovedPackagesRule } from './src/rules/exports_moved_packages'; import { NoUnusedImportsRule } from './src/rules/no_unused_imports'; import { NoBoundaryCrossingRule } from './src/rules/no_boundary_crossing'; +import { NoGroupCrossingImportsRule } from './src/rules/no_group_crossing_imports'; +import { NoGroupCrossingManifestsRule } from './src/rules/no_group_crossing_manifests'; import { RequireImportRule } from './src/rules/require_import'; /** @@ -25,5 +27,7 @@ export const rules = { exports_moved_packages: ExportsMovedPackagesRule, no_unused_imports: NoUnusedImportsRule, no_boundary_crossing: NoBoundaryCrossingRule, + no_group_crossing_imports: NoGroupCrossingImportsRule, + no_group_crossing_manifests: NoGroupCrossingManifestsRule, require_import: RequireImportRule, }; diff --git a/packages/kbn-eslint-plugin-imports/src/helpers/groups.ts b/packages/kbn-eslint-plugin-imports/src/helpers/groups.ts new file mode 100644 index 0000000000000..a76251f028389 --- /dev/null +++ b/packages/kbn-eslint-plugin-imports/src/helpers/groups.ts @@ -0,0 +1,25 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types'; + +/** + * Checks whether a given ModuleGroup can import from another one + * @param importerGroup The group of the module that we are checking + * @param importedGroup The group of the imported module + * @param importedVisibility The visibility of the imported module + * @returns true if importerGroup is allowed to import from importedGroup/Visibiliy + */ +export function isImportableFrom( + importerGroup: ModuleGroup, + importedGroup: ModuleGroup, + importedVisibility: ModuleVisibility +): boolean { + return importerGroup === importedGroup || importedVisibility === 'shared'; +} diff --git a/packages/kbn-eslint-plugin-imports/src/helpers/report.ts b/packages/kbn-eslint-plugin-imports/src/helpers/report.ts index 9ac0171507efd..11fc09fbecab3 100644 --- a/packages/kbn-eslint-plugin-imports/src/helpers/report.ts +++ b/packages/kbn-eslint-plugin-imports/src/helpers/report.ts @@ -30,3 +30,19 @@ export function report(context: Rule.RuleContext, options: ReportOptions) { : null, }); } + +export const toList = (strings: string[]) => { + const items = strings.map((s) => `"${s}"`); + const list = items.slice(0, -1).join(', '); + const last = items.at(-1); + return !list.length ? last ?? '' : `${list} or ${last}`; +}; + +export const formatSuggestions = (suggestions: string[]) => { + const s = suggestions.map((l) => l.trim()).filter(Boolean); + if (!s.length) { + return ''; + } + + return ` \nSuggestions:\n - ${s.join('\n - ')}\n\n`; +}; diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.test.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.test.ts index be9e60978fa88..f44c0571b2c94 100644 --- a/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.test.ts +++ b/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.test.ts @@ -9,8 +9,9 @@ import { RuleTester } from 'eslint'; import { NoBoundaryCrossingRule } from './no_boundary_crossing'; -import { ModuleType } from '@kbn/repo-source-classifier'; +import type { ModuleType } from '@kbn/repo-source-classifier'; import dedent from 'dedent'; +import { formatSuggestions } from '../helpers/report'; const make = (from: ModuleType, to: ModuleType, imp = 'import') => ({ filename: `${from}.ts`, @@ -107,13 +108,12 @@ for (const [name, tester] of [tsTester, babelTester]) { data: { importedType: 'server package', ownType: 'common package', - suggestion: ` ${dedent` - Suggestions: - - Remove the import statement. - - Limit your imports to "common package" or "static" code. - - Covert to a type-only import. - - Reach out to #kibana-operations for help. - `}`, + suggestion: formatSuggestions([ + 'Remove the import statement.', + 'Limit your imports to "common package" or "static" code.', + 'Covert to a type-only import.', + 'Reach out to #kibana-operations for help.', + ]), }, }, ], diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.ts index 59c73c1d0336c..3f426e13a6215 100644 --- a/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.ts +++ b/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.ts @@ -12,13 +12,14 @@ import Path from 'path'; import { TSESTree } from '@typescript-eslint/typescript-estree'; import * as Bt from '@babel/types'; import type { Rule } from 'eslint'; -import ESTree from 'estree'; -import { ModuleType } from '@kbn/repo-source-classifier'; +import type { Node } from 'estree'; +import type { ModuleType } from '@kbn/repo-source-classifier'; import { visitAllImportStatements, Importer } from '../helpers/visit_all_import_statements'; import { getSourcePath } from '../helpers/source'; import { getRepoSourceClassifier } from '../helpers/repo_source_classifier'; import { getImportResolver } from '../get_import_resolver'; +import { formatSuggestions, toList } from '../helpers/report'; const ANY = Symbol(); @@ -33,22 +34,6 @@ const IMPORTABLE_FROM: Record = { tooling: ANY, }; -const toList = (strings: string[]) => { - const items = strings.map((s) => `"${s}"`); - const list = items.slice(0, -1).join(', '); - const last = items.at(-1); - return !list.length ? last ?? '' : `${list} or ${last}`; -}; - -const formatSuggestions = (suggestions: string[]) => { - const s = suggestions.map((l) => l.trim()).filter(Boolean); - if (!s.length) { - return ''; - } - - return ` Suggestions:\n - ${s.join('\n - ')}`; -}; - const isTypeOnlyImport = (importer: Importer) => { // handle babel nodes if (Bt.isImportDeclaration(importer)) { @@ -125,7 +110,7 @@ export const NoBoundaryCrossingRule: Rule.RuleModule = { if (!importable.includes(imported.type)) { context.report({ - node: node as ESTree.Node, + node: node as Node, messageId: 'TYPE_MISMATCH', data: { ownType: self.type, diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.test.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.test.ts new file mode 100644 index 0000000000000..dc4828603f73f --- /dev/null +++ b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.test.ts @@ -0,0 +1,155 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { RuleTester } from 'eslint'; +import dedent from 'dedent'; +import { NoGroupCrossingImportsRule } from './no_group_crossing_imports'; +import { formatSuggestions } from '../helpers/report'; +import { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types'; + +const make = ( + fromGroup: ModuleGroup, + fromVisibility: ModuleVisibility, + toGroup: ModuleGroup, + toVisibility: ModuleVisibility, + imp = 'import' +) => ({ + filename: `${fromGroup}.${fromVisibility}.ts`, + code: dedent` + ${imp} '${toGroup}.${toVisibility}' + `, +}); + +jest.mock('../get_import_resolver', () => { + return { + getImportResolver() { + return { + resolve(req: string) { + return { + type: 'file', + absolute: req.split('.'), + }; + }, + }; + }, + }; +}); + +jest.mock('../helpers/repo_source_classifier', () => { + return { + getRepoSourceClassifier() { + return { + classify(r: string | [string, string]) { + const [group, visibility] = + typeof r === 'string' ? (r.endsWith('.ts') ? r.slice(0, -3) : r).split('.') : r; + return { + pkgInfo: { + pkgId: 'aPackage', + }, + group, + visibility, + }; + }, + }; + }, + }; +}); + +const tsTester = [ + '@typescript-eslint/parser', + new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + ecmaFeatures: { + jsx: true, + }, + }, + }), +] as const; + +const babelTester = [ + '@babel/eslint-parser', + new RuleTester({ + parser: require.resolve('@babel/eslint-parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + requireConfigFile: false, + babelOptions: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + }), +] as const; + +for (const [name, tester] of [tsTester, babelTester]) { + describe(name, () => { + tester.run('@kbn/imports/no_group_crossing_imports', NoGroupCrossingImportsRule, { + valid: [ + make('observability', 'private', 'observability', 'private'), + make('security', 'private', 'security', 'private'), + make('search', 'private', 'search', 'private'), + make('observability', 'private', 'platform', 'shared'), + make('security', 'private', 'common', 'shared'), + make('platform', 'shared', 'platform', 'shared'), + make('platform', 'shared', 'platform', 'private'), + make('common', 'shared', 'common', 'shared'), + ], + + invalid: [ + { + ...make('observability', 'private', 'security', 'private'), + errors: [ + { + line: 1, + messageId: 'ILLEGAL_IMPORT', + data: { + importerPackage: 'aPackage', + importerGroup: 'observability', + importedPackage: 'aPackage', + importedGroup: 'security', + importedVisibility: 'private', + sourcePath: 'observability.private.ts', + suggestion: formatSuggestions([ + `Please review the dependencies in your module's manifest (kibana.jsonc).`, + `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`, + `Address the conflicting dependencies by refactoring the code`, + ]), + }, + }, + ], + }, + { + ...make('security', 'private', 'platform', 'private'), + errors: [ + { + line: 1, + messageId: 'ILLEGAL_IMPORT', + data: { + importerPackage: 'aPackage', + importerGroup: 'security', + importedPackage: 'aPackage', + importedGroup: 'platform', + importedVisibility: 'private', + sourcePath: 'security.private.ts', + suggestion: formatSuggestions([ + `Please review the dependencies in your module's manifest (kibana.jsonc).`, + `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`, + `Address the conflicting dependencies by refactoring the code`, + ]), + }, + }, + ], + }, + ], + }); + }); +} diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.ts new file mode 100644 index 0000000000000..255973ab7460a --- /dev/null +++ b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.ts @@ -0,0 +1,77 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { dirname } from 'path'; +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; +import { REPO_ROOT } from '@kbn/repo-info'; + +import { visitAllImportStatements } from '../helpers/visit_all_import_statements'; +import { getSourcePath } from '../helpers/source'; +import { getRepoSourceClassifier } from '../helpers/repo_source_classifier'; +import { getImportResolver } from '../get_import_resolver'; +import { formatSuggestions } from '../helpers/report'; +import { isImportableFrom } from '../helpers/groups'; + +export const NoGroupCrossingImportsRule: Rule.RuleModule = { + meta: { + docs: { + url: 'https://github.com/elastic/kibana/blob/main/packages/kbn-eslint-plugin-imports/README.mdx#kbnimportsno_unused_imports', + }, + messages: { + ILLEGAL_IMPORT: `⚠ Illegal import statement: "{{importerPackage}}" ({{importerGroup}}) is importing "{{importedPackage}}" ({{importedGroup}}/{{importedVisibility}}). File: {{sourcePath}}\n{{suggestion}}\n`, + }, + }, + create(context) { + const resolver = getImportResolver(context); + const classifier = getRepoSourceClassifier(resolver); + const sourcePath = getSourcePath(context); + const ownDirname = dirname(sourcePath); + const self = classifier.classify(sourcePath); + const relativePath = sourcePath.replace(REPO_ROOT, '').replace(/^\//, ''); + + return visitAllImportStatements((req, { node }) => { + if ( + req === null || + // we can ignore imports using the raw-loader, they will need to be resolved but can be managed on a case by case basis + req.startsWith('!!raw-loader') + ) { + return; + } + + const result = resolver.resolve(req, ownDirname); + if (result?.type !== 'file' || result.nodeModule) { + return; + } + + const imported = classifier.classify(result.absolute); + + if (!isImportableFrom(self.group, imported.group, imported.visibility)) { + context.report({ + node: node as Node, + messageId: 'ILLEGAL_IMPORT', + data: { + importerPackage: self.pkgInfo?.pkgId ?? 'unknown', + importerGroup: self.group, + importedPackage: imported.pkgInfo?.pkgId ?? 'unknown', + importedGroup: imported.group, + importedVisibility: imported.visibility, + sourcePath: relativePath, + suggestion: formatSuggestions([ + `Please review the dependencies in your module's manifest (kibana.jsonc).`, + `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`, + `Address the conflicting dependencies by refactoring the code`, + ]), + }, + }); + return; + } + }); + }, +}; diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.test.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.test.ts new file mode 100644 index 0000000000000..bf75a01b222bb --- /dev/null +++ b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.test.ts @@ -0,0 +1,280 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { RuleTester } from 'eslint'; +import dedent from 'dedent'; +import { NoGroupCrossingManifestsRule } from './no_group_crossing_manifests'; +import { formatSuggestions } from '../helpers/report'; +import { ModuleId } from '@kbn/repo-source-classifier/src/module_id'; +import { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types'; + +const makePlugin = (filename: string) => ({ + filename, + code: dedent` + export function plugin() { + return new MyPlugin(); + } + `, +}); + +const makePluginClass = (filename: string) => ({ + filename, + code: dedent` + class MyPlugin implements Plugin { + setup() { + console.log('foo'); + } + start() { + console.log('foo'); + } + } + `, +}); + +const makeModuleByPath = ( + path: string, + group: ModuleGroup, + visibility: ModuleVisibility, + pluginOverrides: any = {} +): Record => { + const pluginId = path.split('/')[4]; + const packageId = `@kbn/${pluginId}-plugin`; + + return { + [path]: { + type: 'server package', + dirs: [], + repoRel: 'some/relative/path', + pkgInfo: { + pkgId: packageId, + pkgDir: path.split('/').slice(0, -2).join('/'), + rel: 'some/relative/path', + }, + group, + visibility, + manifest: { + type: 'plugin', + id: packageId, + owner: ['@kbn/kibana-operations'], + plugin: { + id: pluginId, + browser: true, + server: true, + ...pluginOverrides, + }, + }, + }, + }; +}; + +const makeError = (line: number, ...violations: string[]) => ({ + line, + messageId: 'ILLEGAL_MANIFEST_DEPENDENCY', + data: { + violations: violations.join('\n'), + suggestion: formatSuggestions([ + `Please review the dependencies in your plugin's manifest (kibana.jsonc).`, + `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`, + `Address the conflicting dependencies by refactoring the code`, + ]), + }, +}); + +jest.mock('../helpers/repo_source_classifier', () => { + const MODULES_BY_PATH: Record = { + ...makeModuleByPath( + 'path/to/search/plugins/searchPlugin1/server/index.ts', + 'search', + 'private', + { + requiredPlugins: ['searchPlugin2'], // allowed, same group + } + ), + ...makeModuleByPath( + 'path/to/search/plugins/searchPlugin2/server/index.ts', + 'search', + 'private', + { + requiredPlugins: ['securityPlugin1'], // invalid, dependency belongs to another group + } + ), + ...makeModuleByPath( + 'path/to/security/plugins/securityPlugin1/server/index.ts', + 'security', + 'private', + { + requiredPlugins: ['securityPlugin2'], // allowed, same group + } + ), + ...makeModuleByPath( + 'path/to/security/plugins/securityPlugin2/server/index.ts', + 'security', + 'private', + { + requiredPlugins: ['platformPlugin1', 'platformPlugin2', 'platformPlugin3'], // 3rd one is private! + } + ), + ...makeModuleByPath( + 'path/to/platform/shared/platformPlugin1/server/index.ts', + 'platform', + 'shared', + { + requiredPlugins: ['platformPlugin2', 'platformPlugin3', 'platformPlugin4'], + } + ), + ...makeModuleByPath( + 'path/to/platform/shared/platformPlugin2/server/index.ts', + 'platform', + 'shared' + ), + ...makeModuleByPath( + 'path/to/platform/private/platformPlugin3/server/index.ts', + 'platform', + 'private' + ), + ...makeModuleByPath( + 'path/to/platform/private/platformPlugin4/server/index.ts', + 'platform', + 'private' + ), + }; + + return { + getRepoSourceClassifier() { + return { + classify(path: string) { + return MODULES_BY_PATH[path]; + }, + }; + }, + }; +}); + +jest.mock('@kbn/repo-packages', () => { + const original = jest.requireActual('@kbn/repo-packages'); + + return { + ...original, + getPluginPackagesFilter: () => () => true, + getPackages() { + return [ + 'path/to/search/plugins/searchPlugin1/server/index.ts', + 'path/to/search/plugins/searchPlugin2/server/index.ts', + 'path/to/security/plugins/securityPlugin1/server/index.ts', + 'path/to/security/plugins/securityPlugin2/server/index.ts', + 'path/to/platform/shared/platformPlugin1/server/index.ts', + 'path/to/platform/shared/platformPlugin2/server/index.ts', + 'path/to/platform/private/platformPlugin3/server/index.ts', + 'path/to/platform/private/platformPlugin4/server/index.ts', + ].map((path) => { + const [, , group, , id] = path.split('/'); + return { + id: `@kbn/${id}-plugin`, + group, + visibility: path.includes('platform/shared') ? 'shared' : 'private', + manifest: { + plugin: { + id, + }, + }, + }; + }); + }, + }; +}); + +const tsTester = [ + '@typescript-eslint/parser', + new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + ecmaFeatures: { + jsx: true, + }, + }, + }), +] as const; + +const babelTester = [ + '@babel/eslint-parser', + new RuleTester({ + parser: require.resolve('@babel/eslint-parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + requireConfigFile: false, + babelOptions: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + }), +] as const; + +for (const [name, tester] of [tsTester, babelTester]) { + describe(name, () => { + tester.run('@kbn/imports/no_group_crossing_manifests', NoGroupCrossingManifestsRule, { + valid: [ + makePlugin('path/to/search/plugins/searchPlugin1/server/index.ts'), + makePlugin('path/to/security/plugins/securityPlugin1/server/index.ts'), + makePlugin('path/to/platform/shared/platformPlugin1/server/index.ts'), + makePluginClass('path/to/search/plugins/searchPlugin1/server/index.ts'), + makePluginClass('path/to/security/plugins/securityPlugin1/server/index.ts'), + makePluginClass('path/to/platform/shared/platformPlugin1/server/index.ts'), + ], + invalid: [ + { + ...makePlugin('path/to/search/plugins/searchPlugin2/server/index.ts'), + errors: [ + makeError( + 1, + `⚠ Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc` + ), + ], + }, + { + ...makePlugin('path/to/security/plugins/securityPlugin2/server/index.ts'), + errors: [ + makeError( + 1, + `⚠ Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc` + ), + ], + }, + { + ...makePluginClass('path/to/search/plugins/searchPlugin2/server/index.ts'), + errors: [ + makeError( + 2, + `⚠ Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc` + ), + makeError( + 5, + `⚠ Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc` + ), + ], + }, + { + ...makePluginClass('path/to/security/plugins/securityPlugin2/server/index.ts'), + errors: [ + makeError( + 2, + `⚠ Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc` + ), + makeError( + 5, + `⚠ Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc` + ), + ], + }, + ], + }); + }); +} diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.ts new file mode 100644 index 0000000000000..e68f7217905a5 --- /dev/null +++ b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.ts @@ -0,0 +1,158 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { join } from 'path'; +import { TSESTree } from '@typescript-eslint/typescript-estree'; +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; +import { getPackages, getPluginPackagesFilter } from '@kbn/repo-packages'; +import { REPO_ROOT } from '@kbn/repo-info'; +import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types'; +import { getSourcePath } from '../helpers/source'; +import { getImportResolver } from '../get_import_resolver'; +import { getRepoSourceClassifier } from '../helpers/repo_source_classifier'; +import { isImportableFrom } from '../helpers/groups'; +import { formatSuggestions } from '../helpers/report'; + +const NODE_TYPES = TSESTree.AST_NODE_TYPES; + +interface PluginInfo { + id: string; + pluginId: string; + group: ModuleGroup; + visibility: ModuleVisibility; +} + +export const NoGroupCrossingManifestsRule: Rule.RuleModule = { + meta: { + docs: { + url: 'https://github.com/elastic/kibana/blob/main/packages/kbn-eslint-plugin-imports/README.mdx#kbnimportsno_unused_imports', + }, + messages: { + ILLEGAL_MANIFEST_DEPENDENCY: `{{violations}}\n{{suggestion}}`, + }, + }, + create(context) { + const sourcePath = getSourcePath(context); + let manifestPath: string; + const resolver = getImportResolver(context); + const classifier = getRepoSourceClassifier(resolver); + const moduleId = classifier.classify(sourcePath); + const offendingDependencies: PluginInfo[] = []; + let currentPlugin: PluginInfo; + + if (moduleId.manifest?.type === 'plugin') { + manifestPath = join(moduleId.pkgInfo!.pkgDir, 'kibana.jsonc') + .replace(REPO_ROOT, '') + .replace(/^\//, ''); + currentPlugin = { + id: moduleId.pkgInfo!.pkgId, + pluginId: moduleId.manifest.plugin.id, + group: moduleId.group, + visibility: moduleId.visibility, + }; + + const allPlugins = getPackages(REPO_ROOT).filter(getPluginPackagesFilter()); + const currentPluginInfo = moduleId.manifest!.plugin; + // check all the dependencies in the manifest, looking for plugin violations + [ + ...(currentPluginInfo.requiredPlugins ?? []), + ...(currentPluginInfo.requiredBundles ?? []), + ...(currentPluginInfo.optionalPlugins ?? []), + ...(currentPluginInfo.runtimePluginDependencies ?? []), + ].forEach((pluginId) => { + const dependency = allPlugins.find(({ manifest }) => manifest.plugin.id === pluginId); + if (dependency) { + // at this point, we know the dependency is a plugin + const { id, group, visibility } = dependency; + if (!isImportableFrom(moduleId.group, group, visibility)) { + offendingDependencies.push({ id, pluginId, group, visibility }); + } + } + }); + } + + return { + FunctionDeclaration(node) { + // complain in exported plugin() function + if ( + currentPlugin && + offendingDependencies.length && + node.id?.name === 'plugin' && + node.parent.type === NODE_TYPES.ExportNamedDeclaration + ) { + reportViolation({ + context, + node, + currentPlugin, + manifestPath, + offendingDependencies, + }); + } + }, + MethodDefinition(node) { + // complain in setup() and start() hooks + if ( + offendingDependencies.length && + node.key.type === NODE_TYPES.Identifier && + (node.key.name === 'setup' || node.key.name === 'start') && + node.kind === 'method' && + node.parent.parent.type === NODE_TYPES.ClassDeclaration && + (node.parent.parent.id?.name.includes('Plugin') || + (node.parent.parent as TSESTree.ClassDeclaration).implements?.find( + (value) => + value.expression.type === NODE_TYPES.Identifier && + value.expression.name === 'Plugin' + )) + ) { + reportViolation({ + context, + node, + currentPlugin, + manifestPath, + offendingDependencies, + }); + } + }, + }; + }, +}; + +interface ReportViolationParams { + context: Rule.RuleContext; + node: Node; + currentPlugin: PluginInfo; + offendingDependencies: PluginInfo[]; + manifestPath: string; +} + +const reportViolation = ({ + context, + node, + currentPlugin, + offendingDependencies, + manifestPath, +}: ReportViolationParams) => + context.report({ + node, + messageId: 'ILLEGAL_MANIFEST_DEPENDENCY', + data: { + violations: [ + ...offendingDependencies.map( + ({ id, pluginId, group, visibility }) => + `⚠ Illegal dependency on manifest: Plugin "${currentPlugin.pluginId}" (package: "${currentPlugin.id}"; group: "${currentPlugin.group}") depends on "${pluginId}" (package: "${id}"; group: ${group}/${visibility}). File: ${manifestPath}` + ), + ].join('\n'), + suggestion: formatSuggestions([ + `Please review the dependencies in your plugin's manifest (kibana.jsonc).`, + `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`, + `Address the conflicting dependencies by refactoring the code`, + ]), + }, + }); diff --git a/packages/kbn-eslint-plugin-imports/tsconfig.json b/packages/kbn-eslint-plugin-imports/tsconfig.json index 087d77fbfe437..b0ab9182171c3 100644 --- a/packages/kbn-eslint-plugin-imports/tsconfig.json +++ b/packages/kbn-eslint-plugin-imports/tsconfig.json @@ -14,6 +14,7 @@ "@kbn/import-resolver", "@kbn/repo-source-classifier", "@kbn/repo-info", + "@kbn/repo-packages", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-generate/src/commands/codeowners_command.ts b/packages/kbn-generate/src/commands/codeowners_command.ts index 79f7025b99a02..a86b4250d6850 100644 --- a/packages/kbn-generate/src/commands/codeowners_command.ts +++ b/packages/kbn-generate/src/commands/codeowners_command.ts @@ -63,7 +63,11 @@ export const CodeownersCommand: GenerateCommand = { } const newCodeowners = `${GENERATED_START}${pkgs - .map((pkg) => `${pkg.normalizedRepoRelativeDir} ${pkg.manifest.owner.join(' ')}`) + .map( + (pkg) => + pkg.normalizedRepoRelativeDir + + (pkg.manifest.owner.length ? ' ' + pkg.manifest.owner.join(' ') : '') + ) .join('\n')}${GENERATED_END}${content}${ULTIMATE_PRIORITY_RULES}`; if (newCodeowners === oldCodeowners) { diff --git a/packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts b/packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts index 441df3948632b..30682d763e0b0 100644 --- a/packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts +++ b/packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts @@ -48,6 +48,20 @@ export const MANIFEST_V2: JSONSchema = { For additional codeowners, the value can be an array of user/team names. `, }, + group: { + enum: ['common', 'platform', 'observability', 'security', 'search'], + description: desc` + Specifies the group to which this module pertains. + `, + default: 'common', + }, + visibility: { + enum: ['private', 'shared'], + description: desc` + Specifies the visibility of this module, i.e. whether it can be accessed by everybody or only modules in the same group + `, + default: 'shared', + }, devOnly: { type: 'boolean', description: desc` diff --git a/packages/kbn-manifest/README.md b/packages/kbn-manifest/README.md new file mode 100644 index 0000000000000..a7dc2054252dc --- /dev/null +++ b/packages/kbn-manifest/README.md @@ -0,0 +1,30 @@ +# @kbn/manifest + +This package contains a CLI to list `kibana.jsonc` manifests and also to mass update their properties. + +## Usage + +To list all `kibana.jsonc` manifests, run the following command from the root of the Kibana repo: + +```sh +node scripts/manifest --list all +``` + +To print a manifest by packageId or by pluginId, run the following command from the root of the Kibana repo: + +```sh +node scripts/manifest --package @kbn/package_name +node scripts/manifest --plugin pluginId +``` + +To update properties in one or more manifest files, run the following command from the root of the Kibana repo: + +```sh +node scripts/manifest \ +--package @kbn/package_1 \ +--package @kbn/package_2 \ +# ... +--package @kbn/package_N \ +--set path.to.property1=value \ +--set property2=value +``` diff --git a/packages/kbn-manifest/index.ts b/packages/kbn-manifest/index.ts new file mode 100644 index 0000000000000..5fc4727a1a72d --- /dev/null +++ b/packages/kbn-manifest/index.ts @@ -0,0 +1,46 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { run } from '@kbn/dev-cli-runner'; +import { listManifestFiles, printManifest, updateManifest } from './manifest'; + +/** + * A CLI to manipulate Kibana package manifest files + */ +export const runKbnManifestCli = () => { + run( + async ({ log, flags }) => { + if (flags.list === 'all') { + listManifestFiles(flags, log); + } else { + if (!flags.package && !flags.plugin) { + throw new Error('You must specify the identifer of the --package or --plugin to update.'); + } + await updateManifest(flags, log); + await printManifest(flags, log); + } + }, + { + log: { + defaultLevel: 'info', + }, + flags: { + string: ['list', 'package', 'plugin', 'set', 'unset'], + help: ` + Usage: node scripts/manifest --package --set group=platform --set visibility=private + --list all List all the manifests + --package [packageId] Select a package to update. + --plugin [pluginId] Select a plugin to update. + --set [property] [value] Set the desired "[property]": "[value]" + --unset [property] Removes the desired "[property]: value" from the manifest + `, + }, + } + ); +}; diff --git a/packages/kbn-manifest/jest.config.js b/packages/kbn-manifest/jest.config.js new file mode 100644 index 0000000000000..ed8288d9fb712 --- /dev/null +++ b/packages/kbn-manifest/jest.config.js @@ -0,0 +1,14 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-manifest'], +}; diff --git a/packages/kbn-manifest/kibana.jsonc b/packages/kbn-manifest/kibana.jsonc new file mode 100644 index 0000000000000..27f2d95e65501 --- /dev/null +++ b/packages/kbn-manifest/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-server", + "id": "@kbn/manifest", + "owner": "@elastic/kibana-core" +} diff --git a/packages/kbn-manifest/manifest.ts b/packages/kbn-manifest/manifest.ts new file mode 100644 index 0000000000000..a839dba7b4077 --- /dev/null +++ b/packages/kbn-manifest/manifest.ts @@ -0,0 +1,113 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { join } from 'path'; +import { writeFile } from 'fs/promises'; +import { flatMap, unset } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type { Flags } from '@kbn/dev-cli-runner'; +import { type Package, getPackages } from '@kbn/repo-packages'; +import { REPO_ROOT } from '@kbn/repo-info'; + +const MANIFEST_FILE = 'kibana.jsonc'; + +const getKibanaJsonc = (flags: Flags, log: ToolingLog): Package[] => { + const modules = getPackages(REPO_ROOT); + + let packageIds: string[] = []; + let pluginIds: string[] = []; + + if (typeof flags.package === 'string') { + packageIds = [flags.package].filter(Boolean); + } else if (Array.isArray(flags.package)) { + packageIds = [...flags.package].filter(Boolean); + } + + if (typeof flags.plugin === 'string') { + pluginIds = [flags.plugin].filter(Boolean); + } else if (Array.isArray(flags.plugin)) { + pluginIds = [...flags.plugin].filter(Boolean); + } + + return modules.filter( + (pkg) => + packageIds.includes(pkg.id) || (pkg.isPlugin() && pluginIds.includes(pkg.manifest.plugin.id)) + ); +}; + +export const listManifestFiles = (flags: Flags, log: ToolingLog) => { + const modules = getPackages(REPO_ROOT); + modules + .filter((module) => module.manifest.type === 'plugin') + .forEach((module) => { + log.info(join(module.directory, MANIFEST_FILE), module.id); + }); +}; + +export const printManifest = (flags: Flags, log: ToolingLog) => { + const kibanaJsoncs = getKibanaJsonc(flags, log); + kibanaJsoncs.forEach((kibanaJsonc) => { + const manifestPath = join(kibanaJsonc.directory, MANIFEST_FILE); + log.info('\n\nShowing manifest: ', manifestPath); + log.info(JSON.stringify(kibanaJsonc, null, 2)); + }); +}; + +export const updateManifest = async (flags: Flags, log: ToolingLog) => { + let toSet: string[] = []; + let toUnset: string[] = []; + + if (typeof flags.set === 'string') { + toSet = [flags.set].filter(Boolean); + } else if (Array.isArray(flags.set)) { + toSet = [...flags.set].filter(Boolean); + } + + if (typeof flags.unset === 'string') { + toUnset = [flags.unset].filter(Boolean); + } else if (Array.isArray(flags.unset)) { + toUnset = [...flags.unset].filter(Boolean); + } + + if (!toSet.length && !toUnset.length) { + // no need to update anything + return; + } + + const kibanaJsoncs = getKibanaJsonc(flags, log); + + for (let i = 0; i < kibanaJsoncs.length; ++i) { + const kibanaJsonc = kibanaJsoncs[i]; + + if (kibanaJsonc?.manifest) { + const manifestPath = join(kibanaJsonc.directory, MANIFEST_FILE); + log.info('Updating manifest: ', manifestPath); + toSet.forEach((propValue) => { + const [prop, value] = propValue.split('='); + log.info(`Setting "${prop}": "${value}"`); + set(kibanaJsonc.manifest, prop, value); + }); + + toUnset.forEach((prop) => { + log.info(`Removing "${prop}"`); + unset(kibanaJsonc.manifest, prop); + }); + + sanitiseManifest(kibanaJsonc); + + await writeFile(manifestPath, JSON.stringify(kibanaJsonc.manifest, null, 2)); + log.info('DONE'); + } + } +}; + +const sanitiseManifest = (kibanaJsonc: Package) => { + kibanaJsonc.manifest.owner = flatMap(kibanaJsonc.manifest.owner.map((owner) => owner.split(' '))); +}; diff --git a/packages/kbn-manifest/package.json b/packages/kbn-manifest/package.json new file mode 100644 index 0000000000000..52304cc4c1e21 --- /dev/null +++ b/packages/kbn-manifest/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/manifest", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} diff --git a/packages/kbn-manifest/tsconfig.json b/packages/kbn-manifest/tsconfig.json new file mode 100644 index 0000000000000..1ee41aafca1ee --- /dev/null +++ b/packages/kbn-manifest/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/dev-cli-runner", + "@kbn/repo-info", + "@kbn/repo-packages", + "@kbn/safer-lodash-set", + "@kbn/tooling-log", + ] +} diff --git a/packages/kbn-repo-info/types.ts b/packages/kbn-repo-info/types.ts index a4776c28760a2..338881e878fdc 100644 --- a/packages/kbn-repo-info/types.ts +++ b/packages/kbn-repo-info/types.ts @@ -7,6 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +export type ModuleGroup = 'platform' | 'observability' | 'search' | 'security' | 'common'; +export type ModuleVisibility = 'private' | 'shared'; + export interface KibanaPackageJson { name: string; version: string; @@ -27,4 +30,6 @@ export interface KibanaPackageJson { [name: string]: string | undefined; }; [key: string]: unknown; + group?: ModuleGroup; + visibility?: ModuleVisibility; } diff --git a/packages/kbn-repo-packages/modern/package.js b/packages/kbn-repo-packages/modern/package.js index 1c44cd0cf86d9..3ec33a69e841a 100644 --- a/packages/kbn-repo-packages/modern/package.js +++ b/packages/kbn-repo-packages/modern/package.js @@ -116,6 +116,22 @@ class Package { * @readonly */ this.id = manifest.id; + + const { group, visibility } = this.determineGroupAndVisibility(); + + /** + * the group to which this package belongs + * @type {import('@kbn/repo-info/types').ModuleGroup} + * @readonly + */ + + this.group = group; + /** + * the visibility of this package, i.e. whether it can be accessed by everybody or only modules in the same group + * @type {import('@kbn/repo-info/types').ModuleVisibility} + * @readonly + */ + this.visibility = visibility; } /** @@ -140,6 +156,24 @@ class Package { return this.manifest.type === 'plugin'; } + /** + * Returns the group to which this package belongs + * @readonly + * @returns {import('@kbn/repo-info/types').ModuleGroup} + */ + getGroup() { + return this.group; + } + + /** + * Returns the package visibility, i.e. whether it can be accessed by everybody or only packages in the same group + * @readonly + * @returns {import('@kbn/repo-info/types').ModuleVisibility} + */ + getVisibility() { + return this.visibility; + } + /** * Returns true if the package represents some type of plugin * @returns {import('./types').PluginCategoryInfo} @@ -158,6 +192,7 @@ class Package { const oss = !dir.startsWith('x-pack/'); const example = dir.startsWith('examples/') || dir.startsWith('x-pack/examples/'); const testPlugin = dir.startsWith('test/') || dir.startsWith('x-pack/test/'); + return { oss, example, @@ -165,6 +200,40 @@ class Package { }; } + determineGroupAndVisibility() { + const dir = this.normalizedRepoRelativeDir; + + /** @type {import('@kbn/repo-info/types').ModuleGroup} */ + let group = 'common'; + /** @type {import('@kbn/repo-info/types').ModuleVisibility} */ + let visibility = 'shared'; + + if (dir.startsWith('src/platform/') || dir.startsWith('x-pack/platform/')) { + group = 'platform'; + visibility = + /src\/platform\/[^\/]+\/shared/.test(dir) || /x-pack\/platform\/[^\/]+\/shared/.test(dir) + ? 'shared' + : 'private'; + } else if (dir.startsWith('x-pack/solutions/search/')) { + group = 'search'; + visibility = 'private'; + } else if (dir.startsWith('x-pack/solutions/security/')) { + group = 'security'; + visibility = 'private'; + } else if (dir.startsWith('x-pack/solutions/observability/')) { + group = 'observability'; + visibility = 'private'; + } else { + group = this.manifest.group ?? 'common'; + // if the group is 'private-only', enforce it + visibility = ['search', 'security', 'observability'].includes(group) + ? 'private' + : this.manifest.visibility ?? 'shared'; + } + + return { group, visibility }; + } + /** * Custom inspect handler so that logging variables in scripts/generate doesn't * print all the BUILD.bazel files diff --git a/packages/kbn-repo-packages/modern/parse_package_manifest.js b/packages/kbn-repo-packages/modern/parse_package_manifest.js index 40a6f7bf1059b..46004983848bb 100644 --- a/packages/kbn-repo-packages/modern/parse_package_manifest.js +++ b/packages/kbn-repo-packages/modern/parse_package_manifest.js @@ -225,16 +225,20 @@ function validatePackageManifest(parsed, repoRoot, path) { type, id, owner, + group, + visibility, devOnly, - plugin, - sharedBrowserBundle, build, description, serviceFolders, ...extra - } = parsed; + } = /** @type {import('./types').PackageManifestBaseFields} */ (/** @type {unknown} */ (parsed)); - const extraKeys = Object.keys(extra); + const { plugin, sharedBrowserBundle } = parsed; + + const extraKeys = Object.keys(extra).filter( + (key) => !['plugin', 'sharedBrowserBundle'].includes(key) + ); if (extraKeys.length) { throw new Error(`unexpected keys in package manifest [${extraKeys.join(', ')}]`); } @@ -258,6 +262,25 @@ function validatePackageManifest(parsed, repoRoot, path) { ); } + if ( + group !== undefined && + (!isSomeString(group) || + !['platform', 'search', 'security', 'observability', 'common'].includes(group)) + ) { + throw err( + `plugin.group`, + group, + `must have a valid value ("platform" | "search" | "security" | "observability" | "common")` + ); + } + + if ( + visibility !== undefined && + (!isSomeString(visibility) || !['private', 'shared'].includes(visibility)) + ) { + throw err(`plugin.visibility`, visibility, `must have a valid value ("private" | "shared")`); + } + if (devOnly !== undefined && typeof devOnly !== 'boolean') { throw err(`devOnly`, devOnly, `must be a boolean when defined`); } @@ -273,6 +296,8 @@ function validatePackageManifest(parsed, repoRoot, path) { const base = { id, owner: Array.isArray(owner) ? owner : [owner], + group, + visibility, devOnly, build: validatePackageManifestBuild(build), description, diff --git a/packages/kbn-repo-packages/modern/types.ts b/packages/kbn-repo-packages/modern/types.ts index 41250de7c6346..c883e33d82497 100644 --- a/packages/kbn-repo-packages/modern/types.ts +++ b/packages/kbn-repo-packages/modern/types.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types'; import type { Package } from './package'; import type { PLUGIN_CATEGORY } from './plugin_category_info'; @@ -44,7 +45,7 @@ export type KibanaPackageType = | 'functional-tests' | 'test-helper'; -interface PackageManifestBaseFields { +export interface PackageManifestBaseFields { /** * The type of this package. Package types define how a package can and should * be used/built. Some package types also change the way that packages are @@ -91,6 +92,14 @@ interface PackageManifestBaseFields { * @deprecated */ serviceFolders?: string[]; + /** + * Specifies the group to which this package belongs + */ + group?: ModuleGroup; + /** + * Specifies the package visibility, i.e. whether it can be accessed by everybody or only packages in the same group + */ + visibility?: ModuleVisibility; } export interface PluginPackageManifest extends PackageManifestBaseFields { diff --git a/packages/kbn-repo-packages/tsconfig.json b/packages/kbn-repo-packages/tsconfig.json index 19c7e8d59f651..be62cc1a4c90b 100644 --- a/packages/kbn-repo-packages/tsconfig.json +++ b/packages/kbn-repo-packages/tsconfig.json @@ -14,5 +14,8 @@ ], "exclude": [ "target/**/*", + ], + "kbn_references": [ + "@kbn/repo-info", ] } diff --git a/packages/kbn-repo-source-classifier/src/group.ts b/packages/kbn-repo-source-classifier/src/group.ts new file mode 100644 index 0000000000000..8103d5c82c590 --- /dev/null +++ b/packages/kbn-repo-source-classifier/src/group.ts @@ -0,0 +1,63 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types'; + +interface ModuleAttrs { + group: ModuleGroup; + visibility: ModuleVisibility; +} + +const DEFAULT_MODULE_ATTRS: ModuleAttrs = { + group: 'common', + visibility: 'shared', +}; + +const MODULE_GROUPING_BY_PATH: Record = { + 'src/platform/plugins/shared': { + group: 'platform', + visibility: 'shared', + }, + 'src/platform/plugins/internal': { + group: 'platform', + visibility: 'private', + }, + 'x-pack/platform/plugins/shared': { + group: 'platform', + visibility: 'shared', + }, + 'x-pack/platform/plugins/internal': { + group: 'platform', + visibility: 'private', + }, + 'x-pack/solutions/observability/plugins': { + group: 'observability', + visibility: 'private', + }, + 'x-pack/solutions/security/plugins': { + group: 'security', + visibility: 'private', + }, + 'x-pack/solutions/search/plugins': { + group: 'search', + visibility: 'private', + }, +}; + +/** + * Determine a plugin's grouping information based on the path where it is defined + * @param packageRelativePath the path in the repo where the package is located + * @returns The grouping information that corresponds to the given path + */ +export function inferGroupAttrsFromPath(packageRelativePath: string): ModuleAttrs { + const grouping = Object.entries(MODULE_GROUPING_BY_PATH).find(([chunk]) => + packageRelativePath.startsWith(chunk) + )?.[1]; + return grouping ?? DEFAULT_MODULE_ATTRS; +} diff --git a/packages/kbn-repo-source-classifier/src/module_id.ts b/packages/kbn-repo-source-classifier/src/module_id.ts index 6af8ece2438fa..284ffe26de0db 100644 --- a/packages/kbn-repo-source-classifier/src/module_id.ts +++ b/packages/kbn-repo-source-classifier/src/module_id.ts @@ -7,16 +7,24 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ModuleType } from './module_type'; -import { PkgInfo } from './pkg_info'; +import type { KibanaPackageManifest } from '@kbn/repo-packages'; +import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types'; +import type { ModuleType } from './module_type'; +import type { PkgInfo } from './pkg_info'; export interface ModuleId { /** Type of the module */ type: ModuleType; + /** Specifies the group to which this module belongs */ + group: ModuleGroup; + /** Specifies the module visibility, i.e. whether it can be accessed by everybody or only modules in the same group */ + visibility: ModuleVisibility; /** repo relative path to the module's source file */ repoRel: string; /** info about the package the source file is within, in the case the file is found within a package */ pkgInfo?: PkgInfo; + /** The type of package, as described in the manifest */ + manifest?: KibanaPackageManifest; /** path segments of the dirname of this */ dirs: string[]; } diff --git a/packages/kbn-repo-source-classifier/src/repo_source_classifier.ts b/packages/kbn-repo-source-classifier/src/repo_source_classifier.ts index 470dd3c424421..c0ab29f659ebd 100644 --- a/packages/kbn-repo-source-classifier/src/repo_source_classifier.ts +++ b/packages/kbn-repo-source-classifier/src/repo_source_classifier.ts @@ -7,11 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ImportResolver } from '@kbn/import-resolver'; -import { ModuleId } from './module_id'; -import { ModuleType } from './module_type'; +import type { ImportResolver } from '@kbn/import-resolver'; +import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types'; +import type { KibanaPackageManifest } from '@kbn/repo-packages/modern/types'; +import type { ModuleId } from './module_id'; +import type { ModuleType } from './module_type'; import { RANDOM_TEST_FILE_NAMES, TEST_DIR, TEST_TAG } from './config'; import { RepoPath } from './repo_path'; +import { inferGroupAttrsFromPath } from './group'; const STATIC_EXTS = new Set( 'json|woff|woff2|ttf|eot|svg|ico|png|jpg|gif|jpeg|html|md|txt|tmpl|xml' @@ -231,7 +234,43 @@ export class RepoSourceClassifier { return 'common package'; } - classify(absolute: string) { + private getManifest(path: RepoPath): KibanaPackageManifest | undefined { + const pkgInfo = path.getPkgInfo(); + return pkgInfo?.pkgId ? this.resolver.getPkgManifest(pkgInfo!.pkgId) : undefined; + } + /** + * Determine the "group" of a file + */ + private getGroup(path: RepoPath): ModuleGroup { + const attrs = inferGroupAttrsFromPath(path.getRepoRel()); + const manifest = this.getManifest(path); + + if (attrs.group !== 'common') { + // this package has been moved to a 'group-specific' folder, the group is determined by its location + return attrs.group; + } else { + // the package is still in its original location, allow Manifest to dictate its group + return manifest?.group ?? 'common'; + } + } + + /** + * Determine the "visibility" of a file + */ + private getVisibility(path: RepoPath): ModuleVisibility { + const attrs = inferGroupAttrsFromPath(path.getRepoRel()); + const manifest = this.getManifest(path); + + if (attrs.group !== 'common') { + // this package has been moved to a 'group-specific' folder, the visibility is determined by its location + return attrs.visibility; + } else { + // the package is still in its original location, allow Manifest to dictate its visibility + return manifest?.visibility ?? 'shared'; + } + } + + classify(absolute: string): ModuleId { const path = this.getRepoPath(absolute); const cached = this.ids.get(path); @@ -241,8 +280,12 @@ export class RepoSourceClassifier { const id: ModuleId = { type: this.getType(path), + group: this.getGroup(path), + visibility: this.getVisibility(path), repoRel: path.getRepoRel(), pkgInfo: path.getPkgInfo() ?? undefined, + manifest: + (path.getPkgInfo() && this.resolver.getPkgManifest(path.getPkgInfo()!.pkgId)) ?? undefined, dirs: path.getSegs(), }; this.ids.set(path, id); diff --git a/packages/kbn-repo-source-classifier/tsconfig.json b/packages/kbn-repo-source-classifier/tsconfig.json index f41dffcd32f06..418b114eebafa 100644 --- a/packages/kbn-repo-source-classifier/tsconfig.json +++ b/packages/kbn-repo-source-classifier/tsconfig.json @@ -13,6 +13,7 @@ "kbn_references": [ "@kbn/import-resolver", "@kbn/repo-info", + "@kbn/repo-packages", ], "exclude": [ "target/**/*", diff --git a/scripts/manifest.js b/scripts/manifest.js new file mode 100644 index 0000000000000..f9da9c3d174bd --- /dev/null +++ b/scripts/manifest.js @@ -0,0 +1,11 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +require('../src/setup_node_env'); +require('@kbn/manifest').runKbnManifestCli(); diff --git a/tsconfig.base.json b/tsconfig.base.json index 09d1f31eceb23..5028780367b9c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1188,6 +1188,8 @@ "@kbn/management-storybook-config/*": ["packages/kbn-management/storybook/config/*"], "@kbn/management-test-plugin": ["test/plugin_functional/plugins/management_test_plugin"], "@kbn/management-test-plugin/*": ["test/plugin_functional/plugins/management_test_plugin/*"], + "@kbn/manifest": ["packages/kbn-manifest"], + "@kbn/manifest/*": ["packages/kbn-manifest/*"], "@kbn/mapbox-gl": ["packages/kbn-mapbox-gl"], "@kbn/mapbox-gl/*": ["packages/kbn-mapbox-gl/*"], "@kbn/maps-custom-raster-source-plugin": ["x-pack/examples/third_party_maps_source_example"], diff --git a/yarn.lock b/yarn.lock index 0e0d6afb677c2..b5b1294c39f7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5671,6 +5671,10 @@ version "0.0.0" uid "" +"@kbn/manifest@link:packages/kbn-manifest": + version "0.0.0" + uid "" + "@kbn/mapbox-gl@link:packages/kbn-mapbox-gl": version "0.0.0" uid "" From c9637cf71c97e2290db57302d54b90caffb6b1bf Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Tue, 22 Oct 2024 14:07:25 +0200 Subject: [PATCH 04/12] Use more efficient strategies to process user input (#196858) ## Summary Address performance concerns with Regexps --- .../src/static_assets/util.ts | 16 ++++++++-- .../src/actions/es_errors.test.ts | 29 ++++++++++++++++++ .../src/actions/es_errors.ts | 30 +++++++++++++------ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/packages/core/http/core-http-server-internal/src/static_assets/util.ts b/packages/core/http/core-http-server-internal/src/static_assets/util.ts index 9cd9213805b23..0bcc738582f2b 100644 --- a/packages/core/http/core-http-server-internal/src/static_assets/util.ts +++ b/packages/core/http/core-http-server-internal/src/static_assets/util.ts @@ -14,11 +14,23 @@ function isEmptyPathname(pathname: string): boolean { } function removeTailSlashes(pathname: string): string { - return pathname.replace(/\/+$/, ''); + let updated = pathname; + + while (updated.endsWith('/')) { + updated = updated.substring(0, updated.length - 1); + } + + return updated; } function removeLeadSlashes(pathname: string): string { - return pathname.replace(/^\/+/, ''); + let updated = pathname; + + while (updated.startsWith('/')) { + updated = updated.substring(1); + } + + return updated; } export function removeSurroundingSlashes(pathname: string): string { diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.test.ts index 9a93f50487bcd..73a0fc0659939 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.test.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.test.ts @@ -8,6 +8,7 @@ */ import { + hasAllKeywordsInOrder, isClusterShardLimitExceeded, isIncompatibleMappingException, isIndexNotFoundException, @@ -128,3 +129,31 @@ describe('isClusterShardLimitExceeded', () => { expect(isClusterShardLimitExceeded(undefined)).toEqual(false); }); }); + +describe('hasAllKeywordsInOrder', () => { + it('returns false if not all keywords are present', () => { + expect( + hasAllKeywordsInOrder('some keywords in a message', ['some', 'in', 'message', 'missing']) + ).toEqual(false); + }); + + it('returns false if keywords are not in the right order', () => { + expect( + hasAllKeywordsInOrder('some keywords in a message', ['some', 'message', 'keywords']) + ).toEqual(false); + }); + + it('returns false if the message is empty', () => { + expect(hasAllKeywordsInOrder('', ['some', 'message', 'keywords'])).toEqual(false); + }); + + it('returns false if the keyword list is empty', () => { + expect(hasAllKeywordsInOrder('some keywords in a message', [])).toEqual(false); + }); + + it('returns true if keywords are present and in the right order', () => { + expect( + hasAllKeywordsInOrder('some keywords in a message', ['some', 'keywords', 'in', 'message']) + ).toEqual(true); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.ts index fbded8ad44b29..0ea6ccc227cba 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.ts @@ -7,16 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ErrorCause } from '@elastic/elasticsearch/lib/api/types'; -export const isWriteBlockException = (errorCause?: estypes.ErrorCause): boolean => { +export const isWriteBlockException = (errorCause?: ErrorCause): boolean => { return ( errorCause?.type === 'cluster_block_exception' && - errorCause?.reason?.match(/index \[.+] blocked by: \[FORBIDDEN\/8\/.+ \(api\)\]/) !== null + hasAllKeywordsInOrder(errorCause?.reason, ['index [', '] blocked by: [FORBIDDEN/8/', ' (api)]']) ); }; -export const isIncompatibleMappingException = (errorCause?: estypes.ErrorCause): boolean => { +export const isIncompatibleMappingException = (errorCause?: ErrorCause): boolean => { return ( errorCause?.type === 'strict_dynamic_mapping_exception' || errorCause?.type === 'mapper_parsing_exception' || @@ -24,17 +24,29 @@ export const isIncompatibleMappingException = (errorCause?: estypes.ErrorCause): ); }; -export const isIndexNotFoundException = (errorCause?: estypes.ErrorCause): boolean => { +export const isIndexNotFoundException = (errorCause?: ErrorCause): boolean => { return errorCause?.type === 'index_not_found_exception'; }; -export const isClusterShardLimitExceeded = (errorCause?: estypes.ErrorCause): boolean => { +export const isClusterShardLimitExceeded = (errorCause?: ErrorCause): boolean => { // traditional ES: validation_exception. serverless ES: illegal_argument_exception return ( (errorCause?.type === 'validation_exception' || errorCause?.type === 'illegal_argument_exception') && - errorCause?.reason?.match( - /this action would add .* shards, but this cluster currently has .* maximum normal shards open/ - ) !== null + hasAllKeywordsInOrder(errorCause?.reason, [ + 'this action would add', + 'shards, but this cluster currently has', + 'maximum normal shards open', + ]) ); }; + +export const hasAllKeywordsInOrder = (message: string | undefined, keywords: string[]): boolean => { + if (!message || !keywords.length) { + return false; + } + + const keywordIndices = keywords.map((keyword) => message?.indexOf(keyword) ?? -1); + // check that all keywords are present and in the right order + return keywordIndices.every((v, i, a) => v >= 0 && (!i || a[i - 1] <= v)); +}; From b668544406aa45df70c5c3e1f88ccf2b1c0eb140 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 22 Oct 2024 08:12:08 -0400 Subject: [PATCH 05/12] [Fleet] Improve space selector validation when not providing valid space (#197117) --- .../agent_policy_advanced_fields/index.tsx | 43 ++++----- .../space_selector.test.tsx | 90 +++++++++++++++++++ .../space_selector.tsx | 61 +++++++++---- .../components/agent_policy_form.tsx | 10 ++- .../components/settings/index.tsx | 8 +- .../components/create_agent_policy.tsx | 10 ++- 6 files changed, 176 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.test.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 1497b1bb0589e..6b0a7c512d197 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -53,7 +53,7 @@ import { UninstallCommandFlyout } from '../../../../../../components'; import type { ValidationResults } from '../agent_policy_validation'; import { ExperimentalFeaturesService } from '../../../../services'; - +import { useAgentPolicyFormContext } from '../agent_policy_form'; import { policyHasEndpointSecurity as hasElasticDefend } from '../../../../../../../common/services'; import { @@ -127,6 +127,8 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = const isManagedorAgentlessPolicy = agentPolicy.is_managed === true || agentPolicy?.supports_agentless === true; + const agentPolicyFormContect = useAgentPolicyFormContext(); + const AgentTamperProtectionSectionContent = useMemo( () => ( = /> } > - - { + if (newValue.length === 0) { + return; } - onChange={(newValue) => { - if (newValue.length === 0) { - return; - } - updateAgentPolicy({ - space_ids: newValue, - }); - }} - /> - + updateAgentPolicy({ + space_ids: newValue, + }); + }} + /> ) : null} { + beforeEach(() => { + jest.mocked(useAgentPoliciesSpaces).mockReturnValue({ + data: { + items: [ + { + name: 'Default', + id: 'default', + }, + { + name: 'Test', + id: 'test', + }, + ], + }, + } as any); + }); + function render() { + const renderer = createFleetTestRendererMock(); + const onChange = jest.fn(); + const setInvalidSpaceError = jest.fn(); + const result = renderer.render( + + ); + + return { + result, + onChange, + setInvalidSpaceError, + }; + } + + it('should render invalid space errors', () => { + const { result, onChange, setInvalidSpaceError } = render(); + const inputEl = result.getByTestId('comboBoxSearchInput'); + fireEvent.change(inputEl, { + target: { value: 'invalidSpace' }, + }); + fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' }); + expect(result.container).toHaveTextContent('invalidSpace is not a valid space.'); + expect(onChange).not.toBeCalled(); + expect(setInvalidSpaceError).toBeCalledWith(true); + }); + + it('should clear invalid space errors', () => { + const { result, setInvalidSpaceError } = render(); + const inputEl = result.getByTestId('comboBoxSearchInput'); + fireEvent.change(inputEl, { + target: { value: 'invalidSpace' }, + }); + fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' }); + expect(result.container).toHaveTextContent('invalidSpace is not a valid space.'); + fireEvent.change(inputEl, { + target: { value: '' }, + }); + fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' }); + expect(result.container).not.toHaveTextContent('invalidSpace is not a valid space.'); + expect(setInvalidSpaceError).toBeCalledWith(false); + }); + + it('should accept valid space', () => { + const { result, onChange, setInvalidSpaceError } = render(); + const inputEl = result.getByTestId('comboBoxSearchInput'); + fireEvent.change(inputEl, { + target: { value: 'test' }, + }); + fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' }); + expect(result.container).not.toHaveTextContent('test is not a valid space.'); + expect(onChange).toBeCalledWith(['test']); + expect(setInvalidSpaceError).not.toBeCalledWith(true); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.tsx index 0532c5306d50f..53c7ed1d8226d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { type EuiComboBoxOptionOption, EuiHealth } from '@elastic/eui'; +import { type EuiComboBoxOptionOption, EuiHealth, EuiFormRow } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; @@ -16,11 +16,19 @@ export interface SpaceSelectorProps { value: string[]; onChange: (newVal: string[]) => void; isDisabled?: boolean; + setInvalidSpaceError?: (hasError: boolean) => void; } -export const SpaceSelector: React.FC = ({ value, onChange, isDisabled }) => { +export const SpaceSelector: React.FC = ({ + setInvalidSpaceError, + value, + onChange, + isDisabled, +}) => { const res = useAgentPoliciesSpaces(); + const [error, setError] = React.useState(); + const renderOption = React.useCallback( (option: any, searchValue: string, contentClassName: string) => ( @@ -57,20 +65,41 @@ export const SpaceSelector: React.FC = ({ value, onChange, i }, [options, value, res.isInitialLoading]); return ( - { - onChange(newOptions.map(({ key }) => key as string)); - }} - /> + key="space" + error={error} + isDisabled={isDisabled} + isInvalid={Boolean(error)} + > + { + const newError = + searchValue.length === 0 || hasMatchingOptions + ? undefined + : i18n.translate('xpack.fleet.agentPolicies.spaceSelectorInvalid', { + defaultMessage: '{space} is not a valid space.', + values: { space: searchValue }, + }); + setError(newError); + if (setInvalidSpaceError) { + setInvalidSpaceError(!!newError); + } + }} + onChange={(newOptions) => { + onChange(newOptions.map(({ key }) => key as string)); + }} + /> + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx index b437d61f64c58..8e97afcaa4d66 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx @@ -45,12 +45,14 @@ interface Props { isEditing?: boolean; // form error state is passed up to the form updateAdvancedSettingsHasErrors: (hasErrors: boolean) => void; + setInvalidSpaceError: (hasErrors: boolean) => void; } const AgentPolicyFormContext = React.createContext< | { agentPolicy: Partial & { [key: string]: any }; updateAgentPolicy: (u: Partial) => void; updateAdvancedSettingsHasErrors: (hasErrors: boolean) => void; + setInvalidSpaceError: (hasErrors: boolean) => void; } | undefined >(undefined); @@ -67,6 +69,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ validation, isEditing = false, updateAdvancedSettingsHasErrors, + setInvalidSpaceError, }) => { const authz = useAuthz(); const isDisabled = !authz.fleet.allAgentPolicies; @@ -97,7 +100,12 @@ export const AgentPolicyForm: React.FunctionComponent = ({ return ( {!isEditing ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index 6e4f1e06b45a0..91cd710db4343 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -90,6 +90,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( allowedNamespacePrefixes: spaceSettings?.allowedNamespacePrefixes, }); const [hasAdvancedSettingsErrors, setHasAdvancedSettingsErrors] = useState(false); + const [hasInvalidSpaceError, setInvalidSpaceError] = useState(false); const updateAgentPolicy = (updatedFields: Partial) => { setAgentPolicy({ @@ -183,6 +184,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( validation={validation} isEditing={true} updateAdvancedSettingsHasErrors={setHasAdvancedSettingsErrors} + setInvalidSpaceError={setInvalidSpaceError} /> {hasChanges ? ( @@ -219,7 +221,8 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( isDisabled={ isLoading || Object.keys(validation).length > 0 || - hasAdvancedSettingsErrors + hasAdvancedSettingsErrors || + hasInvalidSpaceError } btnProps={{ color: 'text', @@ -242,7 +245,8 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( !hasAllAgentPoliciesPrivileges || isLoading || Object.keys(validation).length > 0 || - hasAdvancedSettingsErrors + hasAdvancedSettingsErrors || + hasInvalidSpaceError } data-test-subj="agentPolicyDetailsSaveButton" iconType="save" diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx index f147f7e112ea1..a5538e7e0fa30 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx @@ -61,6 +61,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ allowedNamespacePrefixes: spaceSettings?.allowedNamespacePrefixes, }); const [hasAdvancedSettingsErrors, setHasAdvancedSettingsErrors] = useState(false); + const [hasInvalidSpaceError, setInvalidSpaceError] = useState(false); const updateAgentPolicy = (updatedFields: Partial) => { setAgentPolicy({ @@ -104,6 +105,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ updateSysMonitoring={(newValue) => setWithSysMonitoring(newValue)} validation={validation} updateAdvancedSettingsHasErrors={setHasAdvancedSettingsErrors} + setInvalidSpaceError={setInvalidSpaceError} /> ); @@ -130,7 +132,10 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ 0 || hasAdvancedSettingsErrors + isLoading || + Object.keys(validation).length > 0 || + hasAdvancedSettingsErrors || + hasInvalidSpaceError } description={i18n.translate( 'xpack.fleet.createAgentPolicy.devtoolsRequestDescription', @@ -150,7 +155,8 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ !hasFleetAllAgentPoliciesPrivileges || isLoading || Object.keys(validation).length > 0 || - hasAdvancedSettingsErrors + hasAdvancedSettingsErrors || + hasInvalidSpaceError } onClick={async () => { setIsLoading(true); From 3130492752b622458d521eec228e075916237d74 Mon Sep 17 00:00:00 2001 From: mohamedhamed-ahmed Date: Tue, 22 Oct 2024 13:33:20 +0100 Subject: [PATCH 06/12] =?UTF-8?q?[Discover]=20Use=20summary=20column=20ser?= =?UTF-8?q?vice=20name=20component=20for=20service=20name=E2=80=A6=20(#196?= =?UTF-8?q?742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/elastic/kibana/issues/196541 ## 📝 Summary This PR updated the `service.name` cell renderer so that it mimics what we have in the `summary` column. It now shows a clickable pill shape for quick filters and navigating to the service page if `APM` is available. ## 🎥 Demo https://github.com/user-attachments/assets/627b39af-f008-487b-82f2-c0ab79aff9a4 --- .../logs/service_name_cell.test.tsx | 51 +++++++++++++------ .../data_types/logs/service_name_cell.tsx | 41 ++++++++------- .../accessors/get_cell_renderers.tsx | 4 +- .../extensions/_get_cell_renderers.ts | 20 +++++--- .../extensions/_get_cell_renderers.ts | 20 +++++--- 5 files changed, 90 insertions(+), 46 deletions(-) diff --git a/src/plugins/discover/public/components/data_types/logs/service_name_cell.test.tsx b/src/plugins/discover/public/components/data_types/logs/service_name_cell.test.tsx index 8cf45be4f09e5..3171c5e61e629 100644 --- a/src/plugins/discover/public/components/data_types/logs/service_name_cell.test.tsx +++ b/src/plugins/discover/public/components/data_types/logs/service_name_cell.test.tsx @@ -7,15 +7,46 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import React from 'react'; import { buildDataTableRecord, DataTableRecord } from '@kbn/discover-utils'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; import { render, screen } from '@testing-library/react'; -import React from 'react'; +import { DataGridDensity, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table'; import { getServiceNameCell } from './service_name_cell'; +import { CellRenderersExtensionParams } from '../../../context_awareness'; + +const core = { + application: { + capabilities: { + apm: { + show: true, + }, + }, + }, + uiSettings: { + get: () => true, + }, +}; + +jest.mock('../../../hooks/use_discover_services', () => { + const originalModule = jest.requireActual('../../../hooks/use_discover_services'); + return { + ...originalModule, + useDiscoverServices: () => ({ core, share: {} }), + }; +}); const renderCell = (serviceNameField: string, record: DataTableRecord) => { - const ServiceNameCell = getServiceNameCell(serviceNameField); + const cellRenderersExtensionParamsMock: CellRenderersExtensionParams = { + actions: { + addFilter: jest.fn(), + }, + dataView: dataViewMock, + density: DataGridDensity.COMPACT, + rowHeight: ROWS_HEIGHT_OPTIONS.single, + }; + const ServiceNameCell = getServiceNameCell(serviceNameField, cellRenderersExtensionParamsMock); render( { dataViewMock ); renderCell('service.name', record); - expect(screen.getByTestId('serviceNameCell-nodejs')).toBeInTheDocument(); - }); - - it('renders default icon with unknwon test subject if agent name is missing', () => { - const record = buildDataTableRecord( - { fields: { 'service.name': 'test-service' } }, - dataViewMock - ); - renderCell('service.name', record); - expect(screen.getByTestId('serviceNameCell-unknown')).toBeInTheDocument(); + expect(screen.getByTestId('dataTableCellActionsPopover_service.name')).toBeInTheDocument(); }); - it('does not render if service name is missing', () => { + it('does render empty div if service name is missing', () => { const record = buildDataTableRecord({ fields: { 'agent.name': 'nodejs' } }, dataViewMock); renderCell('service.name', record); - expect(screen.queryByTestId('serviceNameCell-nodejs')).not.toBeInTheDocument(); - expect(screen.queryByTestId('serviceNameCell-unknown')).not.toBeInTheDocument(); + expect(screen.queryByTestId('serviceNameCell-empty')).toBeInTheDocument(); }); }); diff --git a/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx b/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx index 39d112de5258e..cd94cd609dc69 100644 --- a/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx +++ b/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx @@ -7,19 +7,27 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { EuiToolTip } from '@elastic/eui'; import type { AgentName } from '@kbn/elastic-agent-utils'; import { dynamic } from '@kbn/shared-ux-utility'; import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; -import React from 'react'; +import { css } from '@emotion/react'; import { getFieldValue } from '@kbn/discover-utils'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { CellRenderersExtensionParams } from '../../../context_awareness'; import { AGENT_NAME_FIELD } from '../../../../common/data_types/logs/constants'; +import { ServiceNameBadgeWithActions } from './service_name_badge_with_actions'; -const dataTestSubj = 'serviceNameCell'; const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon')); +const dataTestSubj = 'serviceNameCell'; +const agentIconStyle = css` + margin-right: ${euiThemeVars.euiSizeXS}; +`; export const getServiceNameCell = - (serviceNameField: string) => (props: DataGridCellValueElementProps) => { + (serviceNameField: string, { actions }: CellRenderersExtensionParams) => + (props: DataGridCellValueElementProps) => { const serviceNameValue = getFieldValue(props.row, serviceNameField) as string; const agentName = getFieldValue(props.row, AGENT_NAME_FIELD) as AgentName; @@ -27,19 +35,18 @@ export const getServiceNameCell = return -; } + const getIcon = () => ( + + + + ); + return ( - - - - - - - {serviceNameValue} - + ); }; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx b/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx index 9e45892070120..7e13baf8ddcf9 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx @@ -31,8 +31,8 @@ export const getCellRenderers: DataSourceProfileProvider['profile']['getCellRend ...SERVICE_NAME_FIELDS.reduce( (acc, field) => ({ ...acc, - [field]: getServiceNameCell(field), - [`${field}.keyword`]: getServiceNameCell(`${field}.keyword`), + [field]: getServiceNameCell(field, params), + [`${field}.keyword`]: getServiceNameCell(`${field}.keyword`, params), }), {} ), diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts b/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts index cb66afc7ebc57..e18f6c5860dd2 100644 --- a/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts +++ b/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts @@ -105,8 +105,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 0); const lastCell = await dataGrid.getCellElementExcludingControlColumns(2, 0); - const firstServiceNameCell = await firstCell.findByTestSubject('serviceNameCell-java'); - const lastServiceNameCell = await lastCell.findByTestSubject('serviceNameCell-unknown'); + const firstServiceNameCell = await firstCell.findByTestSubject( + 'dataTableCellActionsPopover_service.name' + ); + const lastServiceNameCell = await lastCell.findByTestSubject( + 'dataTableCellActionsPopover_service.name' + ); expect(await firstServiceNameCell.getVisibleText()).to.be('product'); expect(await lastServiceNameCell.getVisibleText()).to.be('accounting'); }); @@ -130,7 +134,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { const firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 0); expect(await firstCell.getVisibleText()).to.be('product'); - await testSubjects.missingOrFail('*serviceNameCell*'); + await testSubjects.missingOrFail('dataTableCellActionsPopover_service.name'); }); }); }); @@ -278,8 +282,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); lastCell = await dataGrid.getCellElementExcludingControlColumns(2, 1); - const firstServiceNameCell = await firstCell.findByTestSubject('serviceNameCell-java'); - const lastServiceNameCell = await lastCell.findByTestSubject('serviceNameCell-unknown'); + const firstServiceNameCell = await firstCell.findByTestSubject( + 'dataTableCellActionsPopover_service.name' + ); + const lastServiceNameCell = await lastCell.findByTestSubject( + 'dataTableCellActionsPopover_service.name' + ); expect(await firstServiceNameCell.getVisibleText()).to.be('product'); expect(await lastServiceNameCell.getVisibleText()).to.be('accounting'); }); @@ -309,7 +317,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await firstCell.getVisibleText()).to.be('product'); expect(await lastCell.getVisibleText()).to.be('accounting'); - await testSubjects.missingOrFail('*serviceNameCell*'); + await testSubjects.missingOrFail('dataTableCellActionsPopover_service.name'); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts index 0cf8aeedd257a..b8503e0f8dcab 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts @@ -105,8 +105,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 0); const lastCell = await dataGrid.getCellElementExcludingControlColumns(2, 0); - const firstServiceNameCell = await firstCell.findByTestSubject('serviceNameCell-java'); - const lastServiceNameCell = await lastCell.findByTestSubject('serviceNameCell-unknown'); + const firstServiceNameCell = await firstCell.findByTestSubject( + 'dataTableCellActionsPopover_service.name' + ); + const lastServiceNameCell = await lastCell.findByTestSubject( + 'dataTableCellActionsPopover_service.name' + ); expect(await firstServiceNameCell.getVisibleText()).to.be('product'); expect(await lastServiceNameCell.getVisibleText()).to.be('accounting'); }); @@ -130,7 +134,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { const firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 0); expect(await firstCell.getVisibleText()).to.be('product'); - await testSubjects.missingOrFail('*serviceNameCell*'); + await testSubjects.missingOrFail('dataTableCellActionsPopover_service.name'); }); }); }); @@ -277,8 +281,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); lastCell = await dataGrid.getCellElementExcludingControlColumns(2, 1); - const firstServiceNameCell = await firstCell.findByTestSubject('serviceNameCell-java'); - const lastServiceNameCell = await lastCell.findByTestSubject('serviceNameCell-unknown'); + const firstServiceNameCell = await firstCell.findByTestSubject( + 'dataTableCellActionsPopover_service.name' + ); + const lastServiceNameCell = await lastCell.findByTestSubject( + 'dataTableCellActionsPopover_service.name' + ); expect(await firstServiceNameCell.getVisibleText()).to.be('product'); expect(await lastServiceNameCell.getVisibleText()).to.be('accounting'); }); @@ -308,7 +316,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await firstCell.getVisibleText()).to.be('product'); expect(await lastCell.getVisibleText()).to.be('accounting'); - await testSubjects.missingOrFail('*serviceNameCell*'); + await testSubjects.missingOrFail('dataTableCellActionsPopover_service.name'); }); }); }); From 3be33bd3e97cc21c13123408e5e01177a6aa600d Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 22 Oct 2024 14:46:24 +0200 Subject: [PATCH 07/12] [Fleet] Display outputs in agent list table and agent details (#195801) Closes https://github.com/elastic/kibana/issues/192339 ## Summary Display two additional columns with Outputs hosts in agent list table and agent details section - The two columns show monitoring output and the integrations output and link to the output flyout in settings - Display a badge that show the outputs set per integration introduced by https://github.com/elastic/kibana/pull/189125 - Same info displayed in agent details as well To achieve this, I added two new endpoints. 1. Endpoint that fetches all the outputs associated with a single agent policy (outputs defined on agent policy or default defined in global settings and if any, outputs per integration) ``` GET kbn:/api/fleet/agent_policies//outputs ``` 2. Endpoint that fetches the outputs as above, for a defined set of agent policy ids ``` POST kbn:/api/fleet/agent_policies/outputs { "ids": ["policy_id1", "policy_id2", ...] } ``` The reason to pass an array of ids is to ensure that we fetch the info only for the policies displayed in the table at any given moment. ### Screenshots **Agent list** ![Screenshot 2024-10-16 at 17 51 57](https://github.com/user-attachments/assets/3ee08df1-9562-497f-9621-4a913b3dad74) ![Screenshot 2024-10-16 at 17 52 05](https://github.com/user-attachments/assets/72b9da7d-872a-45f8-b02d-29184ffb2179) **Agent details** ![Screenshot 2024-10-16 at 17 52 20](https://github.com/user-attachments/assets/b99aaf9e-14f1-44b8-9776-3e0136775af8) ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 ### For maintainers - [ ] This will appear in the **Release Notes** and follow the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Elastic Machine Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- oas_docs/bundle.json | 343 ++++++++++++++++++ oas_docs/bundle.serverless.json | 343 ++++++++++++++++++ .../output/kibana.serverless.staging.yaml | 226 ++++++++++++ oas_docs/output/kibana.serverless.yaml | 226 ++++++++++++ oas_docs/output/kibana.staging.yaml | 226 ++++++++++++ oas_docs/output/kibana.yaml | 226 ++++++++++++ .../plugins/fleet/common/constants/routes.ts | 2 + .../plugins/fleet/common/services/routes.ts | 8 + .../fleet/common/types/models/agent_policy.ts | 21 ++ .../common/types/rest_spec/agent_policy.ts | 20 +- .../agent_details/agent_details_overview.tsx | 28 +- .../components/agent_list_table.tsx | 118 ++++-- .../agent_policy_outputs_summary.test.tsx | 99 +++++ .../agent_policy_outputs_summary.tsx | 115 ++++++ .../public/hooks/use_request/agent_policy.ts | 21 ++ x-pack/plugins/fleet/public/types/index.ts | 3 + .../server/routes/agent_policy/handlers.ts | 65 ++++ .../fleet/server/routes/agent_policy/index.ts | 64 ++++ .../agent_policies/full_agent_policy.test.ts | 2 +- .../agent_policies/related_saved_objects.ts | 2 +- .../fleet/server/services/agent_policy.ts | 94 ++++- .../plugins/fleet/server/services/output.ts | 6 +- .../services/preconfiguration/outputs.ts | 1 - x-pack/plugins/fleet/server/types/index.tsx | 1 + .../fleet/server/types/models/agent_policy.ts | 32 ++ .../server/types/rest_spec/agent_policy.ts | 14 + .../apis/agent_policy/agent_policy_outputs.ts | 282 ++++++++++++++ .../apis/agent_policy/index.js | 1 + 28 files changed, 2547 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.tsx create mode 100644 x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_outputs.ts diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 169133c63753c..098fb1de18699 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -9776,6 +9776,191 @@ ] } }, + "/api/fleet/agent_policies/outputs": { + "post": { + "description": "Get list of outputs associated with agent policies", + "operationId": "%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0", + "parameters": [ + { + "description": "The version of the API to use", + "in": "header", + "name": "elastic-api-version", + "schema": { + "default": "2023-10-31", + "enum": [ + "2023-10-31" + ], + "type": "string" + } + }, + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "properties": { + "ids": { + "description": "list of package policy ids", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "ids" + ], + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "additionalProperties": false, + "properties": { + "agentPolicyId": { + "type": "string" + }, + "data": { + "additionalProperties": false, + "properties": { + "integrations": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "integrationPolicyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "pkgName": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "output": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "output" + ], + "type": "object" + }, + "monitoring": { + "additionalProperties": false, + "properties": { + "output": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "output" + ], + "type": "object" + } + }, + "required": [ + "monitoring", + "data" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + } + } + } + }, + "400": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "description": "Generic Error", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + } + } + }, + "summary": "", + "tags": [ + "Elastic Agent policies" + ] + } + }, "/api/fleet/agent_policies/{agentPolicyId}": { "get": { "description": "Get an agent policy by ID", @@ -12938,6 +13123,164 @@ ] } }, + "/api/fleet/agent_policies/{agentPolicyId}/outputs": { + "get": { + "description": "Get list of outputs associated with agent policy by policy id", + "operationId": "%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0", + "parameters": [ + { + "description": "The version of the API to use", + "in": "header", + "name": "elastic-api-version", + "schema": { + "default": "2023-10-31", + "enum": [ + "2023-10-31" + ], + "type": "string" + } + }, + { + "in": "path", + "name": "agentPolicyId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "properties": { + "item": { + "additionalProperties": false, + "properties": { + "agentPolicyId": { + "type": "string" + }, + "data": { + "additionalProperties": false, + "properties": { + "integrations": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "integrationPolicyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "pkgName": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "output": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "output" + ], + "type": "object" + }, + "monitoring": { + "additionalProperties": false, + "properties": { + "output": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "output" + ], + "type": "object" + } + }, + "required": [ + "monitoring", + "data" + ], + "type": "object" + } + }, + "required": [ + "item" + ], + "type": "object" + } + } + } + }, + "400": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "description": "Generic Error", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + } + } + }, + "summary": "", + "tags": [ + "Elastic Agent policies" + ] + } + }, "/api/fleet/agent_status": { "get": { "description": "Get agent status summary", diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 1d989c69f48d4..479f79922fea8 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -9776,6 +9776,191 @@ ] } }, + "/api/fleet/agent_policies/outputs": { + "post": { + "description": "Get list of outputs associated with agent policies", + "operationId": "%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0", + "parameters": [ + { + "description": "The version of the API to use", + "in": "header", + "name": "elastic-api-version", + "schema": { + "default": "2023-10-31", + "enum": [ + "2023-10-31" + ], + "type": "string" + } + }, + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "properties": { + "ids": { + "description": "list of package policy ids", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "ids" + ], + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "additionalProperties": false, + "properties": { + "agentPolicyId": { + "type": "string" + }, + "data": { + "additionalProperties": false, + "properties": { + "integrations": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "integrationPolicyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "pkgName": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "output": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "output" + ], + "type": "object" + }, + "monitoring": { + "additionalProperties": false, + "properties": { + "output": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "output" + ], + "type": "object" + } + }, + "required": [ + "monitoring", + "data" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + } + } + } + }, + "400": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "description": "Generic Error", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + } + } + }, + "summary": "", + "tags": [ + "Elastic Agent policies" + ] + } + }, "/api/fleet/agent_policies/{agentPolicyId}": { "get": { "description": "Get an agent policy by ID", @@ -12938,6 +13123,164 @@ ] } }, + "/api/fleet/agent_policies/{agentPolicyId}/outputs": { + "get": { + "description": "Get list of outputs associated with agent policy by policy id", + "operationId": "%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0", + "parameters": [ + { + "description": "The version of the API to use", + "in": "header", + "name": "elastic-api-version", + "schema": { + "default": "2023-10-31", + "enum": [ + "2023-10-31" + ], + "type": "string" + } + }, + { + "in": "path", + "name": "agentPolicyId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "properties": { + "item": { + "additionalProperties": false, + "properties": { + "agentPolicyId": { + "type": "string" + }, + "data": { + "additionalProperties": false, + "properties": { + "integrations": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "integrationPolicyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "pkgName": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "output": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "output" + ], + "type": "object" + }, + "monitoring": { + "additionalProperties": false, + "properties": { + "output": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "output" + ], + "type": "object" + } + }, + "required": [ + "monitoring", + "data" + ], + "type": "object" + } + }, + "required": [ + "item" + ], + "type": "object" + } + } + } + }, + "400": { + "content": { + "application/json; Elastic-Api-Version=2023-10-31": { + "schema": { + "additionalProperties": false, + "description": "Generic Error", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + } + } + }, + "summary": "", + "tags": [ + "Elastic Agent policies" + ] + } + }, "/api/fleet/agent_status": { "get": { "description": "Get agent status summary", diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index e7a3e9c42ec7a..b1f6938936fbd 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -14594,6 +14594,110 @@ paths: summary: '' tags: - Elastic Agent policies + /api/fleet/agent_policies/{agentPolicyId}/outputs: + get: + description: Get list of outputs associated with agent policy by policy id + operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0' + parameters: + - description: The version of the API to use + in: header + name: elastic-api-version + schema: + default: '2023-10-31' + enum: + - '2023-10-31' + type: string + - in: path + name: agentPolicyId + required: true + schema: + type: string + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + item: + additionalProperties: false + type: object + properties: + agentPolicyId: + type: string + data: + additionalProperties: false + type: object + properties: + integrations: + items: + additionalProperties: false + type: object + properties: + id: + type: string + integrationPolicyName: + type: string + name: + type: string + pkgName: + type: string + type: array + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + monitoring: + additionalProperties: false + type: object + properties: + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + required: + - monitoring + - data + required: + - item + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + required: + - message + summary: '' + tags: + - Elastic Agent policies /api/fleet/agent_policies/delete: post: description: Delete agent policy by ID @@ -14664,6 +14768,128 @@ paths: summary: '' tags: - Elastic Agent policies + /api/fleet/agent_policies/outputs: + post: + description: Get list of outputs associated with agent policies + operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0' + parameters: + - description: The version of the API to use + in: header + name: elastic-api-version + schema: + default: '2023-10-31' + enum: + - '2023-10-31' + type: string + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + ids: + description: list of package policy ids + items: + type: string + type: array + required: + - ids + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + items: + items: + additionalProperties: false + type: object + properties: + agentPolicyId: + type: string + data: + additionalProperties: false + type: object + properties: + integrations: + items: + additionalProperties: false + type: object + properties: + id: + type: string + integrationPolicyName: + type: string + name: + type: string + pkgName: + type: string + type: array + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + monitoring: + additionalProperties: false + type: object + properties: + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + required: + - monitoring + - data + type: array + required: + - items + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + required: + - message + summary: '' + tags: + - Elastic Agent policies /api/fleet/agent_status: get: description: Get agent status summary diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index e7a3e9c42ec7a..b1f6938936fbd 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -14594,6 +14594,110 @@ paths: summary: '' tags: - Elastic Agent policies + /api/fleet/agent_policies/{agentPolicyId}/outputs: + get: + description: Get list of outputs associated with agent policy by policy id + operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0' + parameters: + - description: The version of the API to use + in: header + name: elastic-api-version + schema: + default: '2023-10-31' + enum: + - '2023-10-31' + type: string + - in: path + name: agentPolicyId + required: true + schema: + type: string + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + item: + additionalProperties: false + type: object + properties: + agentPolicyId: + type: string + data: + additionalProperties: false + type: object + properties: + integrations: + items: + additionalProperties: false + type: object + properties: + id: + type: string + integrationPolicyName: + type: string + name: + type: string + pkgName: + type: string + type: array + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + monitoring: + additionalProperties: false + type: object + properties: + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + required: + - monitoring + - data + required: + - item + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + required: + - message + summary: '' + tags: + - Elastic Agent policies /api/fleet/agent_policies/delete: post: description: Delete agent policy by ID @@ -14664,6 +14768,128 @@ paths: summary: '' tags: - Elastic Agent policies + /api/fleet/agent_policies/outputs: + post: + description: Get list of outputs associated with agent policies + operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0' + parameters: + - description: The version of the API to use + in: header + name: elastic-api-version + schema: + default: '2023-10-31' + enum: + - '2023-10-31' + type: string + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + ids: + description: list of package policy ids + items: + type: string + type: array + required: + - ids + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + items: + items: + additionalProperties: false + type: object + properties: + agentPolicyId: + type: string + data: + additionalProperties: false + type: object + properties: + integrations: + items: + additionalProperties: false + type: object + properties: + id: + type: string + integrationPolicyName: + type: string + name: + type: string + pkgName: + type: string + type: array + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + monitoring: + additionalProperties: false + type: object + properties: + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + required: + - monitoring + - data + type: array + required: + - items + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + required: + - message + summary: '' + tags: + - Elastic Agent policies /api/fleet/agent_status: get: description: Get agent status summary diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 16aa969df06d0..ac76216c78801 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -18023,6 +18023,110 @@ paths: summary: '' tags: - Elastic Agent policies + /api/fleet/agent_policies/{agentPolicyId}/outputs: + get: + description: Get list of outputs associated with agent policy by policy id + operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0' + parameters: + - description: The version of the API to use + in: header + name: elastic-api-version + schema: + default: '2023-10-31' + enum: + - '2023-10-31' + type: string + - in: path + name: agentPolicyId + required: true + schema: + type: string + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + item: + additionalProperties: false + type: object + properties: + agentPolicyId: + type: string + data: + additionalProperties: false + type: object + properties: + integrations: + items: + additionalProperties: false + type: object + properties: + id: + type: string + integrationPolicyName: + type: string + name: + type: string + pkgName: + type: string + type: array + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + monitoring: + additionalProperties: false + type: object + properties: + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + required: + - monitoring + - data + required: + - item + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + required: + - message + summary: '' + tags: + - Elastic Agent policies /api/fleet/agent_policies/delete: post: description: Delete agent policy by ID @@ -18093,6 +18197,128 @@ paths: summary: '' tags: - Elastic Agent policies + /api/fleet/agent_policies/outputs: + post: + description: Get list of outputs associated with agent policies + operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0' + parameters: + - description: The version of the API to use + in: header + name: elastic-api-version + schema: + default: '2023-10-31' + enum: + - '2023-10-31' + type: string + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + ids: + description: list of package policy ids + items: + type: string + type: array + required: + - ids + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + items: + items: + additionalProperties: false + type: object + properties: + agentPolicyId: + type: string + data: + additionalProperties: false + type: object + properties: + integrations: + items: + additionalProperties: false + type: object + properties: + id: + type: string + integrationPolicyName: + type: string + name: + type: string + pkgName: + type: string + type: array + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + monitoring: + additionalProperties: false + type: object + properties: + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + required: + - monitoring + - data + type: array + required: + - items + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + required: + - message + summary: '' + tags: + - Elastic Agent policies /api/fleet/agent_status: get: description: Get agent status summary diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 16aa969df06d0..ac76216c78801 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -18023,6 +18023,110 @@ paths: summary: '' tags: - Elastic Agent policies + /api/fleet/agent_policies/{agentPolicyId}/outputs: + get: + description: Get list of outputs associated with agent policy by policy id + operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0' + parameters: + - description: The version of the API to use + in: header + name: elastic-api-version + schema: + default: '2023-10-31' + enum: + - '2023-10-31' + type: string + - in: path + name: agentPolicyId + required: true + schema: + type: string + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + item: + additionalProperties: false + type: object + properties: + agentPolicyId: + type: string + data: + additionalProperties: false + type: object + properties: + integrations: + items: + additionalProperties: false + type: object + properties: + id: + type: string + integrationPolicyName: + type: string + name: + type: string + pkgName: + type: string + type: array + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + monitoring: + additionalProperties: false + type: object + properties: + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + required: + - monitoring + - data + required: + - item + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + required: + - message + summary: '' + tags: + - Elastic Agent policies /api/fleet/agent_policies/delete: post: description: Delete agent policy by ID @@ -18093,6 +18197,128 @@ paths: summary: '' tags: - Elastic Agent policies + /api/fleet/agent_policies/outputs: + post: + description: Get list of outputs associated with agent policies + operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0' + parameters: + - description: The version of the API to use + in: header + name: elastic-api-version + schema: + default: '2023-10-31' + enum: + - '2023-10-31' + type: string + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + ids: + description: list of package policy ids + items: + type: string + type: array + required: + - ids + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + type: object + properties: + items: + items: + additionalProperties: false + type: object + properties: + agentPolicyId: + type: string + data: + additionalProperties: false + type: object + properties: + integrations: + items: + additionalProperties: false + type: object + properties: + id: + type: string + integrationPolicyName: + type: string + name: + type: string + pkgName: + type: string + type: array + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + monitoring: + additionalProperties: false + type: object + properties: + output: + additionalProperties: false + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + required: + - output + required: + - monitoring + - data + type: array + required: + - items + '400': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + required: + - message + summary: '' + tags: + - Elastic Agent policies /api/fleet/agent_status: get: description: Get agent status summary diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index 9b5c35c3b3ce2..c071c6feecbf8 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -81,6 +81,8 @@ export const AGENT_POLICY_API_ROUTES = { DELETE_PATTERN: `${AGENT_POLICY_API_ROOT}/delete`, FULL_INFO_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/full`, FULL_INFO_DOWNLOAD_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/download`, + LIST_OUTPUTS_PATTERN: `${AGENT_POLICY_API_ROOT}/outputs`, + INFO_OUTPUTS_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/outputs`, }; // Kubernetes Manifest API routes diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index ff1fb4a5ff693..520a71e1bdc0a 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -197,6 +197,14 @@ export const agentPolicyRouteService = { getResetAllPreconfiguredAgentPolicyPath: () => { return PRECONFIGURATION_API_ROUTES.RESET_PATTERN; }, + + getInfoOutputsPath: (agentPolicyId: string) => { + return AGENT_POLICY_API_ROUTES.INFO_OUTPUTS_PATTERN.replace('{agentPolicyId}', agentPolicyId); + }, + + getListOutputsPath: () => { + return AGENT_POLICY_API_ROUTES.LIST_OUTPUTS_PATTERN; + }, }; export const dataStreamRouteService = { diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index ebb1aa3afe7f1..ba1a0b182af72 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -262,3 +262,24 @@ export interface AgentlessApiResponse { id: string; region_id: string; } + +// Definitions for agent policy outputs endpoints +export interface MinimalOutput { + name?: string; + id?: string; +} +export interface IntegrationsOutput extends MinimalOutput { + pkgName?: string; + integrationPolicyName?: string; +} + +export interface OutputsForAgentPolicy { + agentPolicyId?: string; + monitoring: { + output: MinimalOutput; + }; + data: { + output: MinimalOutput; + integrations?: IntegrationsOutput[]; + }; +} diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts index 42f44f7c7271e..7432d1d00e61e 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts @@ -5,7 +5,12 @@ * 2.0. */ -import type { AgentPolicy, NewAgentPolicy, FullAgentPolicy } from '../models'; +import type { + AgentPolicy, + NewAgentPolicy, + FullAgentPolicy, + OutputsForAgentPolicy, +} from '../models'; import type { ListResult, ListWithKuery, BulkGetResult } from './common'; @@ -93,3 +98,16 @@ export type FetchAllAgentPoliciesOptions = Pick< export type FetchAllAgentPolicyIdsOptions = Pick & { spaceId?: string; }; + +export interface GetAgentPolicyOutputsResponse { + item: OutputsForAgentPolicy; +} +export interface GetListAgentPolicyOutputsResponse { + items: OutputsForAgentPolicy[]; +} + +export interface GetListAgentPolicyOutputsRequest { + body: { + ids?: string[]; + }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx index 35fd048cc13cd..2c4113c003841 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; import type { Agent, AgentPolicy } from '../../../../../types'; -import { useAgentVersion } from '../../../../../hooks'; +import { useAgentVersion, useGetInfoOutputsForPolicy } from '../../../../../hooks'; import { ExperimentalFeaturesService, isAgentUpgradeable } from '../../../../../services'; import { AgentPolicySummaryLine } from '../../../../../components'; import { AgentHealth } from '../../../components'; @@ -30,6 +30,7 @@ import { Tags } from '../../../components/tags'; import { formatAgentCPU, formatAgentMemory } from '../../../services/agent_metrics'; import { AgentDashboardLink } from '../agent_dashboard_link'; import { AgentUpgradeStatus } from '../../../agent_list_page/components/agent_upgrade_status'; +import { AgentPolicyOutputsSummary } from '../../../agent_list_page/components/agent_policy_outputs_summary'; // Allows child text to be truncated const FlexItemWithMinWidth = styled(EuiFlexItem)` @@ -43,10 +44,17 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{ const latestAgentVersion = useAgentVersion(); const { displayAgentMetrics } = ExperimentalFeaturesService.get(); + const outputRes = useGetInfoOutputsForPolicy(agentPolicy?.id); + const outputs = outputRes?.data?.item; + return ( - + {displayAgentMetrics && ( @@ -206,6 +214,22 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{ ? agent.local_metadata.host.id : '-', }, + { + title: i18n.translate('xpack.fleet.agentDetails.outputForMonitoringLabel', { + defaultMessage: 'Output for integrations', + }), + description: outputs ? : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.outputForMonitoringLabel', { + defaultMessage: 'Output for monitoring', + }), + description: outputs ? ( + + ) : ( + '-' + ), + }, { title: i18n.translate('xpack.fleet.agentDetails.logLevel', { defaultMessage: 'Logging level', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_list_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_list_table.tsx index 4c6c83dd7145e..d70ed67247207 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_list_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_list_table.tsx @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; +import type { EuiBasicTableColumn } from '@elastic/eui'; import { type CriteriaWithPagination } from '@elastic/eui'; import { EuiBasicTable, @@ -23,20 +24,31 @@ import { isAgentUpgradeable, ExperimentalFeaturesService } from '../../../../ser import { AgentHealth } from '../../components'; import type { Pagination } from '../../../../hooks'; -import { useAgentVersion } from '../../../../hooks'; +import { useAgentVersion, useGetListOutputsForPolicies } from '../../../../hooks'; import { useLink, useAuthz } from '../../../../hooks'; import { AgentPolicySummaryLine } from '../../../../components'; import { Tags } from '../../components/tags'; -import type { AgentMetrics } from '../../../../../../../common/types'; +import type { AgentMetrics, OutputsForAgentPolicy } from '../../../../../../../common/types'; import { formatAgentCPU, formatAgentMemory } from '../../services/agent_metrics'; +import { AgentPolicyOutputsSummary } from './agent_policy_outputs_summary'; + import { AgentUpgradeStatus } from './agent_upgrade_status'; import { EmptyPrompt } from './empty_prompt'; -const VERSION_FIELD = 'local_metadata.elastic.agent.version'; -const HOSTNAME_FIELD = 'local_metadata.host.hostname'; +const AGENTS_TABLE_FIELDS = { + ACTIVE: 'active', + HOSTNAME: 'local_metadata.host.hostname', + POLICY: 'policy_id', + METRICS: 'metrics', + VERSION: 'local_metadata.elastic.agent.version', + LAST_CHECKIN: 'last_checkin', + OUTPUT_INTEGRATION: 'output_integrations', + OUTPUT_MONITORING: 'output_monitoring', +}; + function safeMetadata(val: any) { if (typeof val !== 'string') { return '-'; @@ -96,14 +108,33 @@ export const AgentListTable: React.FC = (props: Props) => { const { getHref } = useLink(); const latestAgentVersion = useAgentVersion(); - const isAgentSelectable = (agent: Agent) => { - if (!agent.active) return false; - if (!agent.policy_id) return true; + const isAgentSelectable = useCallback( + (agent: Agent) => { + if (!agent.active) return false; + if (!agent.policy_id) return true; - const agentPolicy = agentPoliciesIndexedById[agent.policy_id]; - const isHosted = agentPolicy?.is_managed === true; - return !isHosted; - }; + const agentPolicy = agentPoliciesIndexedById[agent.policy_id]; + const isHosted = agentPolicy?.is_managed === true; + return !isHosted; + }, + [agentPoliciesIndexedById] + ); + + const agentsShown = useMemo(() => { + return totalAgents + ? showUpgradeable + ? agents.filter((agent) => isAgentSelectable(agent) && isAgentUpgradeable(agent)) + : agents + : []; + }, [agents, isAgentSelectable, showUpgradeable, totalAgents]); + + // get the policyIds of the agents shown on the page + const policyIds = useMemo(() => { + return agentsShown.map((agent) => agent?.policy_id ?? ''); + }, [agentsShown]); + const allOutputs = useGetListOutputsForPolicies({ + ids: policyIds, + }); const noItemsMessage = isLoading && isCurrentRequestIncremented ? ( @@ -140,9 +171,9 @@ export const AgentListTable: React.FC = (props: Props) => { }, }; - const columns = [ + const columns: Array> = [ { - field: 'active', + field: AGENTS_TABLE_FIELDS.ACTIVE, sortable: false, width: '85px', name: i18n.translate('xpack.fleet.agentList.statusColumnTitle', { @@ -151,7 +182,7 @@ export const AgentListTable: React.FC = (props: Props) => { render: (active: boolean, agent: any) => , }, { - field: HOSTNAME_FIELD, + field: AGENTS_TABLE_FIELDS.HOSTNAME, sortable: true, name: i18n.translate('xpack.fleet.agentList.hostColumnTitle', { defaultMessage: 'Host', @@ -171,7 +202,7 @@ export const AgentListTable: React.FC = (props: Props) => { ), }, { - field: 'policy_id', + field: AGENTS_TABLE_FIELDS.POLICY, sortable: true, truncateText: true, name: i18n.translate('xpack.fleet.agentList.policyColumnTitle', { @@ -208,7 +239,7 @@ export const AgentListTable: React.FC = (props: Props) => { ...(displayAgentMetrics ? [ { - field: 'metrics', + field: AGENTS_TABLE_FIELDS.METRICS, sortable: false, name: ( = (props: Props) => { ), }, { - field: 'metrics', + field: AGENTS_TABLE_FIELDS.METRICS, sortable: false, name: ( = (props: Props) => { }, ] : []), - { - field: 'last_checkin', + field: AGENTS_TABLE_FIELDS.LAST_CHECKIN, sortable: true, name: i18n.translate('xpack.fleet.agentList.lastCheckinTitle', { defaultMessage: 'Last activity', }), + width: '100px', + render: (lastCheckin: string) => + lastCheckin ? : undefined, + }, + { + field: AGENTS_TABLE_FIELDS.OUTPUT_INTEGRATION, + sortable: true, + truncateText: true, + name: i18n.translate('xpack.fleet.agentList.integrationsOutputTitle', { + defaultMessage: 'Output for integrations', + }), + width: '180px', + render: (outputs: OutputsForAgentPolicy[], agent: Agent) => { + if (!agent?.policy_id) return null; + + const outputsForPolicy = allOutputs?.data?.items.find( + (item) => item.agentPolicyId === agent?.policy_id + ); + return ; + }, + }, + { + field: AGENTS_TABLE_FIELDS.OUTPUT_MONITORING, + sortable: true, + truncateText: true, + name: i18n.translate('xpack.fleet.agentList.monitoringOutputTitle', { + defaultMessage: 'Output for monitoring', + }), width: '180px', - render: (lastCheckin: string, agent: any) => - lastCheckin ? : null, + render: (outputs: OutputsForAgentPolicy[], agent: Agent) => { + if (!agent?.policy_id) return null; + + const outputsForPolicy = allOutputs?.data?.items.find( + (item) => item.agentPolicyId === agent?.policy_id + ); + return ; + }, }, { - field: VERSION_FIELD, + field: AGENTS_TABLE_FIELDS.VERSION, sortable: true, width: '220px', name: i18n.translate('xpack.fleet.agentList.versionTitle', { @@ -322,13 +386,7 @@ export const AgentListTable: React.FC = (props: Props) => { data-test-subj="fleetAgentListTable" loading={isLoading} noItemsMessage={noItemsMessage} - items={ - totalAgents - ? showUpgradeable - ? agents.filter((agent) => isAgentSelectable(agent) && isAgentUpgradeable(agent)) - : agents - : [] - } + items={agentsShown} itemId="id" columns={columns} pagination={{ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.test.tsx new file mode 100644 index 0000000000000..255b2efb94026 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.test.tsx @@ -0,0 +1,99 @@ +/* + * 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 { act, fireEvent } from '@testing-library/react'; +import React from 'react'; + +import { createFleetTestRendererMock } from '../../../../../../mock'; +import type { TestRenderer } from '../../../../../../mock'; + +import type { OutputsForAgentPolicy } from '../../../../../../../common/types'; + +import { AgentPolicyOutputsSummary } from './agent_policy_outputs_summary'; + +describe('MultipleAgentPolicySummaryLine', () => { + let testRenderer: TestRenderer; + const outputsForPolicy: OutputsForAgentPolicy = { + agentPolicyId: 'policy-1', + monitoring: { + output: { + id: 'elasticsearch1', + name: 'Elasticsearch1', + }, + }, + data: { + output: { + id: 'elasticsearch1', + name: 'Elasticsearch1', + }, + }, + }; + const data = { + data: { + output: { + id: 'elasticsearch1', + name: 'Elasticsearch1', + }, + integrations: [ + { + id: 'remote_es1', + name: 'Remote ES', + pkgName: 'ngnix', + integrationPolicyName: 'Nginx-1', + }, + + { + id: 'logstash', + name: 'Logstash-1', + pkgName: 'apache', + integrationPolicyName: 'Apache-1', + }, + ], + }, + }; + + const render = (outputs?: OutputsForAgentPolicy, isMonitoring?: boolean) => + testRenderer.render( + + ); + + beforeEach(() => { + testRenderer = createFleetTestRendererMock(); + }); + + test('it should render the name associated with the default output when the agent policy does not have custom outputs', async () => { + const results = render(outputsForPolicy); + expect(results.container.textContent).toBe('Elasticsearch1'); + expect(results.queryByTestId('outputNameLink')).toBeInTheDocument(); + expect(results.queryByTestId('outputsIntegrationsNumberBadge')).not.toBeInTheDocument(); + }); + + test('it should render the first output name and the badge when there are multiple outputs associated with integrations', async () => { + const results = render({ ...outputsForPolicy, ...data }); + + expect(results.queryByTestId('outputNameLink')).toBeInTheDocument(); + expect(results.queryByTestId('outputsIntegrationsNumberBadge')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(results.getByTestId('outputsIntegrationsNumberBadge')); + }); + expect(results.queryByTestId('outputPopover')).toBeInTheDocument(); + expect(results.queryByTestId('output-integration-0')?.textContent).toContain( + 'Nginx-1: Remote ES' + ); + expect(results.queryByTestId('output-integration-1')?.textContent).toContain( + 'Apache-1: Logstash-1' + ); + }); + + test('it should not render the badge when monitoring is true', async () => { + const results = render({ ...outputsForPolicy, ...data }, true); + + expect(results.queryByTestId('outputNameLink')).toBeInTheDocument(); + expect(results.queryByTestId('outputsIntegrationsNumberBadge')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.tsx new file mode 100644 index 0000000000000..c0b0e5fbfbccc --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.tsx @@ -0,0 +1,115 @@ +/* + * 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 React, { useMemo, useState } from 'react'; + +import type { EuiListGroupItemProps } from '@elastic/eui'; +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiListGroup, + EuiPopover, + EuiPopoverTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useLink } from '../../../../hooks'; +import type { OutputsForAgentPolicy } from '../../../../../../../common/types'; + +const TruncatedEuiLink = styled(EuiLink)` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 120px; +`; + +export const AgentPolicyOutputsSummary: React.FC<{ + outputs?: OutputsForAgentPolicy; + isMonitoring?: boolean; +}> = ({ outputs, isMonitoring }) => { + const { getHref } = useLink(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const closePopover = () => setIsPopoverOpen(false); + + const monitoring = outputs?.monitoring; + const data = outputs?.data; + + const listItems: EuiListGroupItemProps[] = useMemo(() => { + if (!data?.integrations) return []; + + return (data?.integrations || []).map((integration, index) => { + return { + 'data-test-subj': `output-integration-${index}`, + label: `${integration.integrationPolicyName}: ${integration.name}`, + href: getHref('settings_edit_outputs', { outputId: integration?.id ?? '' }), + iconType: 'dot', + }; + }); + }, [getHref, data?.integrations]); + + return ( + + {isMonitoring ? ( + + + {monitoring?.output.name} + + + ) : ( + + + {data?.output.name} + + + )} + + {data?.integrations && data?.integrations.length >= 1 && !isMonitoring && ( + + setIsPopoverOpen(!isPopoverOpen)} + onClickAriaLabel="Open output integrations popover" + > + +{data?.integrations.length} + + + + {i18n.translate('xpack.fleet.AgentPolicyOutputsSummary.popover.title', { + defaultMessage: 'Output for integrations', + })} + +
+ +
+
+
+ )} +
+ ); +}; diff --git a/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts b/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts index 9e4fb2344fc29..e130eae49c6eb 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts @@ -23,6 +23,9 @@ import type { DeleteAgentPolicyRequest, DeleteAgentPolicyResponse, BulkGetAgentPoliciesResponse, + GetAgentPolicyOutputsResponse, + GetListAgentPolicyOutputsResponse, + GetListAgentPolicyOutputsRequest, } from '../../types'; import { useRequest, sendRequest, useConditionalRequest, sendRequestForRq } from './use_request'; @@ -201,3 +204,21 @@ export const sendResetAllPreconfiguredAgentPolicies = () => { version: API_VERSIONS.internal.v1, }); }; + +export const useGetListOutputsForPolicies = (body?: GetListAgentPolicyOutputsRequest['body']) => { + return useRequest({ + path: agentPolicyRouteService.getListOutputsPath(), + method: 'post', + body: JSON.stringify(body), + version: API_VERSIONS.public.v1, + }); +}; + +export const useGetInfoOutputsForPolicy = (agentPolicyId: string | undefined) => { + return useConditionalRequest({ + path: agentPolicyId ? agentPolicyRouteService.getInfoOutputsPath(agentPolicyId) : undefined, + method: 'get', + shouldSendRequest: !!agentPolicyId, + version: API_VERSIONS.public.v1, + } as SendConditionalRequestConfig); +}; diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index 099df2ce5a34f..0f0adaba20b5d 100644 --- a/x-pack/plugins/fleet/public/types/index.ts +++ b/x-pack/plugins/fleet/public/types/index.ts @@ -147,6 +147,9 @@ export type { GetEnrollmentSettingsRequest, GetEnrollmentSettingsResponse, GetSpaceSettingsResponse, + GetAgentPolicyOutputsResponse, + GetListAgentPolicyOutputsRequest, + GetListAgentPolicyOutputsResponse, } from '../../common/types'; export { entries, diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 2c93880c10609..49b5590a2e761 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -33,6 +33,8 @@ import type { BulkGetAgentPoliciesRequestSchema, AgentPolicy, FleetRequestHandlerContext, + GetAgentPolicyOutputsRequestSchema, + GetListAgentPolicyOutputsRequestSchema, } from '../../types'; import type { @@ -47,6 +49,8 @@ import type { GetFullAgentConfigMapResponse, GetFullAgentManifestResponse, BulkGetAgentPoliciesResponse, + GetAgentPolicyOutputsResponse, + GetListAgentPolicyOutputsResponse, } from '../../../common/types'; import { defaultFleetErrorHandler, @@ -678,3 +682,64 @@ export const downloadK8sManifest: FleetRequestHandler< return defaultFleetErrorHandler({ error, response }); } }; + +export const GetAgentPolicyOutputsHandler: FleetRequestHandler< + TypeOf, + undefined +> = async (context, request, response) => { + try { + const coreContext = await context.core; + const soClient = coreContext.savedObjects.client; + const agentPolicy = await agentPolicyService.get(soClient, request.params.agentPolicyId); + + if (!agentPolicy) { + return response.customError({ + statusCode: 404, + body: { message: 'Agent policy not found' }, + }); + } + const outputs = await agentPolicyService.getAllOutputsForPolicy(soClient, agentPolicy); + + const body: GetAgentPolicyOutputsResponse = { + item: outputs, + }; + return response.ok({ + body, + }); + } catch (error) { + return defaultFleetErrorHandler({ error, response }); + } +}; + +export const GetListAgentPolicyOutputsHandler: FleetRequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + try { + const coreContext = await context.core; + const soClient = coreContext.savedObjects.client; + const { ids } = request.body; + + if (!ids) { + return response.ok({ + body: { items: [] }, + }); + } + const agentPolicies = await agentPolicyService.getByIDs(soClient, ids, { + withPackagePolicies: true, + }); + + const outputsList = await agentPolicyService.listAllOutputsForPolicies(soClient, agentPolicies); + + const body: GetListAgentPolicyOutputsResponse = { + items: outputsList, + }; + + return response.ok({ + body, + }); + } catch (error) { + return defaultFleetErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts index 2ed7079deceec..9311f0ae2acca 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts @@ -28,6 +28,10 @@ import { GetFullAgentPolicyResponseSchema, DownloadFullAgentPolicyResponseSchema, GetK8sManifestResponseScheme, + GetAgentPolicyOutputsRequestSchema, + GetAgentPolicyOutputsResponseSchema, + GetListAgentPolicyOutputsResponseSchema, + GetListAgentPolicyOutputsRequestSchema, } from '../../types'; import { K8S_API_ROUTES } from '../../../common/constants'; @@ -47,6 +51,8 @@ import { downloadK8sManifest, getK8sManifest, bulkGetAgentPoliciesHandler, + GetAgentPolicyOutputsHandler, + GetListAgentPolicyOutputsHandler, } from './handlers'; export const registerRoutes = (router: FleetAuthzRouter) => { @@ -390,4 +396,62 @@ export const registerRoutes = (router: FleetAuthzRouter) => { }, downloadK8sManifest ); + + router.versioned + .post({ + path: AGENT_POLICY_API_ROUTES.LIST_OUTPUTS_PATTERN, + fleetAuthz: (authz) => { + return authz.fleet.readAgentPolicies && authz.fleet.readSettings; + }, + description: `Get list of outputs associated with agent policies`, + options: { + tags: ['oas-tag:Elastic Agent policies'], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: GetListAgentPolicyOutputsRequestSchema, + response: { + 200: { + body: () => GetListAgentPolicyOutputsResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, + }, + GetListAgentPolicyOutputsHandler + ); + + router.versioned + .get({ + path: AGENT_POLICY_API_ROUTES.INFO_OUTPUTS_PATTERN, + fleetAuthz: (authz) => { + return authz.fleet.readAgentPolicies && authz.fleet.readSettings; + }, + description: `Get list of outputs associated with agent policy by policy id`, + options: { + tags: ['oas-tag:Elastic Agent policies'], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: GetAgentPolicyOutputsRequestSchema, + response: { + 200: { + body: () => GetAgentPolicyOutputsResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, + }, + GetAgentPolicyOutputsHandler + ); }; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts index fa5522d50802b..609c560906de2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts @@ -109,7 +109,7 @@ jest.mock('../output', () => { getDefaultDataOutputId: async () => 'test-id', getDefaultMonitoringOutputId: async () => 'test-id', get: (soClient: any, id: string): Output => OUTPUTS[id] || OUTPUTS['test-id'], - bulkGet: async (soClient: any, ids: string[]): Promise => { + bulkGet: async (ids: string[]): Promise => { return ids.map((id) => OUTPUTS[id] || OUTPUTS['test-id']); }, }, diff --git a/x-pack/plugins/fleet/server/services/agent_policies/related_saved_objects.ts b/x-pack/plugins/fleet/server/services/agent_policies/related_saved_objects.ts index 52dd34a757693..c10edcfeb6edf 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/related_saved_objects.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/related_saved_objects.ts @@ -48,7 +48,7 @@ export async function fetchRelatedSavedObjects( const [outputs, { host: downloadSourceUri, proxy_id: downloadSourceProxyId }, fleetServerHosts] = await Promise.all([ - outputService.bulkGet(soClient, outputIds, { ignoreNotFound: true }), + outputService.bulkGet(outputIds, { ignoreNotFound: true }), getSourceUriForAgentPolicy(soClient, agentPolicy), getFleetServerHostsForAgentPolicy(soClient, agentPolicy).catch((err) => { appContextService diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 8ce8c9a4291bc..0209ee6edb630 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { chunk, groupBy, isEqual, keyBy, omit, pick } from 'lodash'; +import { chunk, groupBy, isEqual, keyBy, omit, pick, uniq } from 'lodash'; import { v5 as uuidv5 } from 'uuid'; import { dump } from 'js-yaml'; import pMap from 'p-map'; @@ -61,6 +61,7 @@ import type { PostAgentPolicyCreateCallback, PostAgentPolicyUpdateCallback, PreconfiguredAgentPolicy, + OutputsForAgentPolicy, } from '../types'; import { AGENT_POLICY_INDEX, @@ -74,6 +75,7 @@ import type { FetchAllAgentPoliciesOptions, FetchAllAgentPolicyIdsOptions, FleetServerPolicy, + IntegrationsOutput, PackageInfo, } from '../../common/types'; import { @@ -85,6 +87,7 @@ import { HostedAgentPolicyRestrictionRelatedError, PackagePolicyRestrictionRelatedError, AgentlessPolicyExistsRequestError, + OutputNotFoundError, } from '../errors'; import type { FullAgentConfigMap } from '../../common/types/models/agent_cm'; @@ -1777,6 +1780,95 @@ class AgentPolicyService { }); } + // Get all the outputs per agent policy + public async getAllOutputsForPolicy( + soClient: SavedObjectsClientContract, + agentPolicy: AgentPolicy + ) { + const logger = appContextService.getLogger(); + + const [defaultDataOutputId, defaultMonitoringOutputId] = await Promise.all([ + outputService.getDefaultDataOutputId(soClient), + outputService.getDefaultMonitoringOutputId(soClient), + ]); + + if (!defaultDataOutputId) { + throw new OutputNotFoundError('Default output is not setup'); + } + + const dataOutputId = agentPolicy.data_output_id || defaultDataOutputId; + const monitoringOutputId = + agentPolicy.monitoring_output_id || defaultMonitoringOutputId || dataOutputId; + + const outputIds = uniq([dataOutputId, monitoringOutputId]); + + const fetchedOutputs = await outputService.bulkGet(outputIds, { + ignoreNotFound: true, + }); + + const dataOutput = fetchedOutputs.find((output) => output.id === dataOutputId); + const monitoringOutput = fetchedOutputs.find((output) => output.id === monitoringOutputId); + + let integrationsDataOutputs: IntegrationsOutput[] = []; + if (agentPolicy?.package_policies) { + const integrationsWithOutputs = agentPolicy.package_policies.filter( + (pkgPolicy) => !!pkgPolicy?.output_id + ); + integrationsDataOutputs = await pMap( + integrationsWithOutputs, + async (pkgPolicy) => { + if (pkgPolicy?.output_id) { + try { + const output = await outputService.get(soClient, pkgPolicy.output_id); + return { integrationPolicyName: pkgPolicy?.name, id: output.id, name: output.name }; + } catch (error) { + logger.error( + `error while retrieving output with id "${pkgPolicy.output_id}": ${error}` + ); + } + } + return { integrationPolicyName: pkgPolicy?.name, id: pkgPolicy?.output_id ?? '' }; + }, + { + concurrency: 20, + } + ); + } + const outputs: OutputsForAgentPolicy = { + monitoring: { + output: { + name: monitoringOutput?.name ?? '', + id: monitoringOutput?.id ?? '', + }, + }, + data: { + output: { + name: dataOutput?.name ?? '', + id: dataOutput?.id ?? '', + }, + integrations: integrationsDataOutputs ?? [], + }, + }; + return outputs; + } + + public async listAllOutputsForPolicies( + soClient: SavedObjectsClientContract, + agentPolicies: AgentPolicy[] + ) { + const allOutputs: OutputsForAgentPolicy[] = await pMap( + agentPolicies, + async (agentPolicy) => { + const output = await this.getAllOutputsForPolicy(soClient, agentPolicy); + return { agentPolicyId: agentPolicy.id, ...output }; + }, + { + concurrency: 50, + } + ); + return allOutputs; + } + private checkTamperProtectionLicense(agentPolicy: { is_protected?: boolean }): void { if (agentPolicy?.is_protected && !licenseService.isPlatinum()) { throw new FleetUnauthorizedError('Tamper protection requires Platinum license'); diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 283e28417998b..b5041d9f1df37 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -705,11 +705,7 @@ class OutputService { return outputSavedObjectToOutput(newSo); } - public async bulkGet( - soClient: SavedObjectsClientContract, - ids: string[], - { ignoreNotFound = false } = { ignoreNotFound: true } - ) { + public async bulkGet(ids: string[], { ignoreNotFound = false } = { ignoreNotFound: true }) { const res = await this.encryptedSoClient.bulkGet( ids.map((id) => ({ id: outputIdToUuid(id), type: SAVED_OBJECT_TYPE })) ); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts index 452a73282144e..68b4cf2457e26 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts @@ -74,7 +74,6 @@ export async function createOrUpdatePreconfiguredOutputs( } const existingOutputs = await outputService.bulkGet( - soClient, outputs.map(({ id }) => id), { ignoreNotFound: true } ); diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 786db010c8eed..5d118af97f478 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -101,6 +101,7 @@ export type { InstallLatestExecutedState, TemplateAgentPolicyInput, NewPackagePolicyInput, + OutputsForAgentPolicy, } from '../../common/types'; export { ElasticsearchAssetType, KibanaAssetType, KibanaSavedObjectType } from '../../common/types'; export { dataTypes } from '../../common/constants'; diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index 09c32aeb91af6..e82063e775d70 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -393,3 +393,35 @@ export const FullAgentPolicyResponseSchema = schema.object({ }) ), }); +const MinimalOutputSchema = schema.object({ + id: schema.string(), + name: schema.string(), +}); + +const IntegrationsOutputSchema = schema.arrayOf( + schema.object({ + pkgName: schema.maybe(schema.string()), + integrationPolicyName: schema.maybe(schema.string()), + id: schema.maybe(schema.string()), + name: schema.maybe(schema.string()), + }) +); + +const OutputsForAgentPolicySchema = schema.object({ + agentPolicyId: schema.maybe(schema.string()), + monitoring: schema.object({ + output: MinimalOutputSchema, + }), + data: schema.object({ + output: MinimalOutputSchema, + integrations: schema.maybe(IntegrationsOutputSchema), + }), +}); + +export const GetAgentPolicyOutputsResponseSchema = schema.object({ + item: OutputsForAgentPolicySchema, +}); + +export const GetListAgentPolicyOutputsResponseSchema = schema.object({ + items: schema.arrayOf(OutputsForAgentPolicySchema), +}); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts index b5fc70b70af85..6eb42468a6371 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts @@ -171,3 +171,17 @@ export const GetK8sManifestRequestSchema = { export const GetK8sManifestResponseScheme = schema.object({ item: schema.string(), }); + +export const GetAgentPolicyOutputsRequestSchema = { + params: schema.object({ + agentPolicyId: schema.string(), + }), +}; + +export const GetListAgentPolicyOutputsRequestSchema = { + body: schema.object({ + ids: schema.arrayOf(schema.string(), { + meta: { description: 'list of package policy ids' }, + }), + }), +}; diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_outputs.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_outputs.ts new file mode 100644 index 0000000000000..74c5af6b0d811 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_outputs.ts @@ -0,0 +1,282 @@ +/* + * 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 { CreateAgentPolicyResponse } from '@kbn/fleet-plugin/common'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const fleetAndAgents = getService('fleetAndAgents'); + + const createOutput = async ({ + name, + id, + type, + hosts, + }: { + name: string; + id: string; + type: string; + hosts: string[]; + }): Promise => { + const res = await supertest + .post(`/api/fleet/outputs`) + .set('kbn-xsrf', 'xxxx') + .send({ + id, + name, + type, + hosts, + }) + .expect(200); + return res.body.item.id; + }; + + const createAgentPolicy = async ( + name: string, + id: string, + dataOutputId?: string, + monitoringOutputId?: string + ): Promise => { + const res = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name, + id, + namespace: 'default', + ...(dataOutputId ? { data_output_id: dataOutputId } : {}), + ...(monitoringOutputId ? { monitoring_output_id: monitoringOutputId } : {}), + }) + .expect(200); + return res.body.item; + }; + + const createAgentPolicyWithPackagePolicy = async ({ + name, + id, + outputId, + }: { + name: string; + id: string; + outputId?: string; + }): Promise => { + const { body: res } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name, + namespace: 'default', + id, + }) + .expect(200); + + const agentPolicyWithPPId = res.item.id; + // package policy needs to have a custom output_id + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + ...(outputId ? { output_id: outputId } : {}), + policy_id: agentPolicyWithPPId, + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(200); + return res.item; + }; + + let output1Id = ''; + describe('fleet_agent_policies_outputs', () => { + describe('POST /api/fleet/agent_policies/outputs', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + await kibanaServer.savedObjects.cleanStandardList(); + await fleetAndAgents.setup(); + + output1Id = await createOutput({ + name: 'Output 1', + id: 'logstash-output-1', + type: 'logstash', + hosts: ['test.fr:443'], + }); + }); + after(async () => { + await supertest + .delete(`/api/fleet/outputs/${output1Id}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should get a list of outputs by agent policies', async () => { + await createAgentPolicy('Agent policy with default output', 'agent-policy-1'); + await createAgentPolicy( + 'Agent policy with custom output', + 'agent-policy-2', + output1Id, + output1Id + ); + + const outputsPerPoliciesRes = await supertest + .post(`/api/fleet/agent_policies/outputs`) + .set('kbn-xsrf', 'xxxx') + .send({ + ids: ['agent-policy-1', 'agent-policy-2'], + }) + .expect(200); + expect(outputsPerPoliciesRes.body.items).to.eql([ + { + agentPolicyId: 'agent-policy-1', + monitoring: { + output: { + name: 'default', + id: 'fleet-default-output', + }, + }, + data: { + output: { + name: 'default', + id: 'fleet-default-output', + }, + integrations: [], + }, + }, + { + agentPolicyId: 'agent-policy-2', + monitoring: { + output: { + name: 'Output 1', + id: 'logstash-output-1', + }, + }, + data: { + output: { + name: 'Output 1', + id: 'logstash-output-1', + }, + integrations: [], + }, + }, + ]); + // clean up policies + await supertest + .post(`/api/fleet/agent_policies/delete`) + .send({ agentPolicyId: 'agent-policy-1' }) + .set('kbn-xsrf', 'xxxx') + .expect(200); + await supertest + .post(`/api/fleet/agent_policies/delete`) + .send({ agentPolicyId: 'agent-policy-2' }) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + }); + + let output2Id = ''; + describe('GET /api/fleet/agent_policies/{agentPolicyId}/outputs', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + await kibanaServer.savedObjects.cleanStandardList(); + await fleetAndAgents.setup(); + + output2Id = await createOutput({ + name: 'ES Output 1', + id: 'es-output-1', + type: 'elasticsearch', + hosts: ['https://test.fr:8080'], + }); + }); + after(async () => { + await supertest + .delete(`/api/fleet/outputs/${output2Id}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should get the list of outputs related to an agentPolicy id', async () => { + await createAgentPolicy('Agent policy with ES output', 'agent-policy-custom', output2Id); + + const outputsPerPoliciesRes = await supertest + .get(`/api/fleet/agent_policies/agent-policy-custom/outputs`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + expect(outputsPerPoliciesRes.body.item).to.eql({ + monitoring: { + output: { + name: 'default', + id: 'fleet-default-output', + }, + }, + data: { + output: { + name: 'ES Output 1', + id: 'es-output-1', + }, + integrations: [], + }, + }); + + await supertest + .post(`/api/fleet/agent_policies/delete`) + .send({ agentPolicyId: 'agent-policy-custom' }) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should also list the outputs set on integrations if any', async () => { + await createAgentPolicyWithPackagePolicy({ + name: 'Agent Policy with package policy', + id: 'agent-policy-custom-2', + outputId: output2Id, + }); + + const outputsPerPoliciesRes = await supertest + .get(`/api/fleet/agent_policies/agent-policy-custom-2/outputs`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + expect(outputsPerPoliciesRes.body.item).to.eql({ + monitoring: { + output: { + name: 'default', + id: 'fleet-default-output', + }, + }, + data: { + output: { + name: 'default', + id: 'fleet-default-output', + }, + integrations: [ + { + id: 'es-output-1', + integrationPolicyName: 'filetest-1', + name: 'ES Output 1', + }, + ], + }, + }); + + await supertest + .post(`/api/fleet/agent_policies/delete`) + .send({ agentPolicyId: 'agent-policy-custom-2' }) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/index.js b/x-pack/test/fleet_api_integration/apis/agent_policy/index.js index 9ae58b0089942..b036ab9d8103d 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/index.js +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/index.js @@ -13,5 +13,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./privileges')); loadTestFile(require.resolve('./agent_policy_root_integrations')); loadTestFile(require.resolve('./create_standalone_api_key')); + loadTestFile(require.resolve('./agent_policy_outputs')); }); } From b89941f3ab072135cc3c343f083e2669831ea4af Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 07:56:08 -0500 Subject: [PATCH 08/12] [ci] Cache chromedriver (#196614) Chromedriver is currently downloaded at runtime on each agent. We know the expected version of Chrome at image build time, and can re-use the matching driver already installed instead. This sets `XDG_CACHE_HOME` to `$HOME/.cache` to persist the chromedriver installation. Details on the specification can be found at https://specifications.freedesktop.org/basedir-spec/latest/. Other packages, including cypress, playwright, bazelisk and yarn also respect this environment variable, but are already falling back to the `$HOME/.cache` directory. This also removes `CHROMEDRIVER_FORCE_DOWNLOAD`, which I believe is an artifact of legacy code: https://github.com/elastic/kibana/blob/6.7/.ci/packer_cache.sh#L17-L26. At one point node_modules was initially loaded from an archive to speed up bootstrap times. The intent was to redownload chromedriver because the Chrome version on the agent image was upgraded independently of the bootstrap cache, potentially causing version mismatches. The impact of re-downloading was also less significant, as there was less parallelization in favor of large machines running parallel jobs. --- .buildkite/scripts/common/env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index 511f6ead2d43c..2704f894cf2b6 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -8,6 +8,7 @@ KIBANA_DIR=$(pwd) export KIBANA_DIR export XPACK_DIR="$KIBANA_DIR/x-pack" +export XDG_CACHE_HOME="$HOME/.cache" export CACHE_DIR="$HOME/.kibana" export ES_CACHE_DIR="$HOME/.es-snapshot-cache" PARENT_DIR="$(cd "$KIBANA_DIR/.."; pwd)" @@ -110,7 +111,6 @@ export TEST_CORS_SERVER_PORT=6105 if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" export DETECT_CHROMEDRIVER_VERSION=true - export CHROMEDRIVER_FORCE_DOWNLOAD=true else echo "Chrome not detected, installing default chromedriver binary for the package version" fi From c076b7ac45957c2cffb4cbb5f197e8b01a64c8f9 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Zahid Date: Tue, 22 Oct 2024 15:09:48 +0200 Subject: [PATCH 09/12] [Dataset Quality] Check if Obs Logs Explorer accessible before linking Logs Explorer (#197020) Fixes https://github.com/elastic/kibana/issues/196506 ## Summary Before linking to Logs Explorer in Dataset Quality, the PR checks if `observability-logs-explorer` is available and accessible before consuming the `SINGLE_DATASET_LOCATOR_ID` locator. Observability Logs Explorer app is not available in Security and Search solution views. After the fix: https://github.com/user-attachments/assets/ed36806a-0483-4765-a6f1-85936b92d390 There's only one more place, Observability Onboarding, where `SINGLE_DATASET_LOCATOR_ID` is consumed. Which being part of Observability solution view, it can be assumed that Observability Logs Explorer will always be available. ![image](https://github.com/user-attachments/assets/b51bf9b6-a9c4-4fd4-8865-3dda76262a93) ---- The other Observability Logs Explorer locator `ALL_DATASETS_LOCATOR_ID` is only consumed in observability wrapper apps and `apm` and `infra` plugins, all of which are only available under Observability where Observability Logs Explorer is also available. https://github.com/elastic/kibana/blob/68b3267ca2f76d8de4fedef622608d8685e7eceb/packages/deeplinks/observability/locators/observability_logs_explorer.ts#L24 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/hooks/use_redirect_link.ts | 21 +++++++++++++++++-- .../dataset_quality/tsconfig.json | 3 ++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_redirect_link.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_redirect_link.ts index 638af464a87cd..d1e55d488ba5c 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_redirect_link.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_redirect_link.ts @@ -5,8 +5,12 @@ * 2.0. */ +import { map } from 'rxjs'; import { useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { AppStatus } from '@kbn/core-application-browser'; import { + OBSERVABILITY_LOGS_EXPLORER_APP_ID, SINGLE_DATASET_LOCATOR_ID, SingleDatasetLocatorParams, } from '@kbn/deeplinks-observability'; @@ -34,7 +38,7 @@ export const useRedirectLink = ({ sendTelemetry: SendTelemetryFn; }) => { const { - services: { share }, + services: { share, application }, } = useKibanaContextForPlugin(); const { from, to } = timeRangeConfig; @@ -42,12 +46,24 @@ export const useRedirectLink = ({ const logsExplorerLocator = share.url.locators.get(SINGLE_DATASET_LOCATOR_ID); + const isLogsExplorerAppAccessible = useObservable( + application.applications$.pipe( + map( + (apps) => + (apps.get(OBSERVABILITY_LOGS_EXPLORER_APP_ID)?.status ?? AppStatus.inaccessible) === + AppStatus.accessible + ) + ), + false + ); + return useMemo<{ linkProps: RouterLinkProps; navigate: () => void; isLogsExplorerAvailable: boolean; }>(() => { - const isLogsExplorerAvailable = !!logsExplorerLocator && dataStreamStat.type === 'logs'; + const isLogsExplorerAvailable = + isLogsExplorerAppAccessible && !!logsExplorerLocator && dataStreamStat.type === 'logs'; const config = isLogsExplorerAvailable ? buildLogsExplorerConfig({ locator: logsExplorerLocator, @@ -95,6 +111,7 @@ export const useRedirectLink = ({ query, sendTelemetry, share.url.locators, + isLogsExplorerAppAccessible, ]); }; diff --git a/x-pack/plugins/observability_solution/dataset_quality/tsconfig.json b/x-pack/plugins/observability_solution/dataset_quality/tsconfig.json index 934c0e434d9a5..f0d82fadb54ad 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/tsconfig.json +++ b/x-pack/plugins/observability_solution/dataset_quality/tsconfig.json @@ -59,7 +59,8 @@ "@kbn/telemetry-plugin", "@kbn/usage-collection-plugin", "@kbn/rison", - "@kbn/task-manager-plugin" + "@kbn/task-manager-plugin", + "@kbn/core-application-browser" ], "exclude": [ "target/**/*" From cbf7982887decc527455dd8f64477813e8ea2672 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Zahid Date: Tue, 22 Oct 2024 15:11:08 +0200 Subject: [PATCH 10/12] Make sort integration test resilient to network delays. (#196516) Fixes https://github.com/elastic/kibana/issues/182017 ## Summary The PR tries to address the flakiness from the test. Co-authored-by: Elastic Machine --- .../data_source_selector.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts index f571fe4e0e462..ba12ebc153ca8 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts @@ -214,25 +214,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should sort the integrations list by the clicked sorting option', async () => { // Test ascending order - await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc'); - await retry.try(async () => { + await PageObjects.observabilityLogsExplorer.clickSortButtonBy('desc'); + await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc'); const { integrations } = await PageObjects.observabilityLogsExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts); }); // Test descending order - await PageObjects.observabilityLogsExplorer.clickSortButtonBy('desc'); - await retry.try(async () => { + await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc'); + await PageObjects.observabilityLogsExplorer.clickSortButtonBy('desc'); const { integrations } = await PageObjects.observabilityLogsExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts.slice().reverse()); }); // Test back ascending order - await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc'); - await retry.try(async () => { + await PageObjects.observabilityLogsExplorer.clickSortButtonBy('desc'); + await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc'); const { integrations } = await PageObjects.observabilityLogsExplorer.getIntegrations(); expect(integrations).to.eql(initialPackagesTexts); }); From 6d7fecd8258c60255c3cc1a3d7c86bf1876f62b9 Mon Sep 17 00:00:00 2001 From: Bena Kansara <69037875+benakansara@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:24:14 +0200 Subject: [PATCH 11/12] [RCA] Events timeline improvements (#197127) Closes https://github.com/elastic/kibana/issues/197192 - Alert event is shown as per "alert start" time - Events are filtered by the alert group/source information (For now, only filtering by `service.name` for the demo. We need to change the logic to use `OR` when applying filter for group-by fields) - Fixed rule condition chart on investigation page when "rate" aggregation is used --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad --- .../public/hooks/query_key_factory.ts | 4 ++-- .../investigate_app/public/hooks/use_fetch_events.ts | 5 ++++- .../components/events_timeline/events_timeline.tsx | 12 ++++++++++++ .../investigate_app/server/services/get_events.ts | 2 +- .../investigate_app/tsconfig.json | 1 + .../public/utils/investigation_item_helper.ts | 12 +++++++++--- 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts index 0b625c5681bcb..38e4c90aebe09 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts @@ -12,8 +12,8 @@ export const investigationKeys = { userProfiles: (profileIds: Set) => [...investigationKeys.all, 'userProfiles', ...profileIds] as const, tags: () => [...investigationKeys.all, 'tags'] as const, - events: (rangeFrom?: string, rangeTo?: string) => - [...investigationKeys.all, 'events', rangeFrom, rangeTo] as const, + events: (rangeFrom?: string, rangeTo?: string, filter?: string) => + [...investigationKeys.all, 'events', rangeFrom, rangeTo, filter] as const, stats: () => [...investigationKeys.all, 'stats'] as const, lists: () => [...investigationKeys.all, 'list'] as const, list: (params: { page: number; perPage: number; search?: string; filter?: string }) => diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_events.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_events.ts index 61b0c441c1fc2..5b885fc664b13 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_events.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_events.ts @@ -23,9 +23,11 @@ export interface Response { export function useFetchEvents({ rangeFrom, rangeTo, + filter, }: { rangeFrom?: string; rangeTo?: string; + filter?: string; }): Response { const { core: { @@ -35,12 +37,13 @@ export function useFetchEvents({ } = useKibana(); const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ - queryKey: investigationKeys.events(rangeFrom, rangeTo), + queryKey: investigationKeys.events(rangeFrom, rangeTo, filter), queryFn: async ({ signal }) => { return await http.get(`/api/observability/events`, { query: { rangeFrom, rangeTo, + filter, }, version: '2023-10-31', signal, diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx index 70f4159924bd1..befa50bcc0e8d 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx @@ -11,6 +11,9 @@ import { Chart, Axis, AreaSeries, Position, ScaleType, Settings } from '@elastic import { useActiveCursor } from '@kbn/charts-plugin/public'; import { EuiSkeletonText } from '@elastic/eui'; import { getBrushData } from '@kbn/observability-utils/chart/utils'; +import { Group } from '@kbn/observability-alerting-rule-utils'; +import { ALERT_GROUP } from '@kbn/rule-data-utils'; +import { SERVICE_NAME } from '@kbn/observability-shared-plugin/common'; import { AnnotationEvent } from './annotation_event'; import { TIME_LINE_THEME } from './timeline_theme'; import { useFetchEvents } from '../../../../hooks/use_fetch_events'; @@ -24,10 +27,19 @@ export const EventsTimeLine = () => { const baseTheme = dependencies.start.charts.theme.useChartsBaseTheme(); const { globalParams, updateInvestigationParams } = useInvestigation(); + const { alert } = useInvestigation(); + + const filter = useMemo(() => { + const group = (alert?.[ALERT_GROUP] as unknown as Group[])?.find( + ({ field }) => field === SERVICE_NAME + ); + return group ? `{"${SERVICE_NAME}":"${alert?.[SERVICE_NAME]}"}` : ''; + }, [alert]); const { data: events, isLoading } = useFetchEvents({ rangeFrom: globalParams.timeRange.from, rangeTo: globalParams.timeRange.to, + filter, }); const chartRef = useRef(null); diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/get_events.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/get_events.ts index 6b081f51dfee8..53f42f4c6c057 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/get_events.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/get_events.ts @@ -95,7 +95,7 @@ export async function getAlertEvents( id: _source[ALERT_UUID], title: `${_source[ALERT_RULE_CATEGORY]} breached`, description: _source[ALERT_REASON], - timestamp: new Date(_source['@timestamp']).getTime(), + timestamp: new Date(_source[ALERT_START] as string).getTime(), eventType: 'alert', alertStatus: _source[ALERT_STATUS], }; diff --git a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json index 7ea8234fba670..a853456b1156c 100644 --- a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json @@ -68,5 +68,6 @@ "@kbn/ml-random-sampler-utils", "@kbn/charts-plugin", "@kbn/observability-utils", + "@kbn/observability-alerting-rule-utils", ], } diff --git a/x-pack/plugins/observability_solution/observability/public/utils/investigation_item_helper.ts b/x-pack/plugins/observability_solution/observability/public/utils/investigation_item_helper.ts index cddf3290ed370..91bfcd2ab4bb1 100644 --- a/x-pack/plugins/observability_solution/observability/public/utils/investigation_item_helper.ts +++ b/x-pack/plugins/observability_solution/observability/public/utils/investigation_item_helper.ts @@ -24,9 +24,15 @@ const genLensEqForCustomThresholdRule = (criterion: MetricExpression) => { criterion.metrics.forEach((metric: CustomThresholdExpressionMetric) => { const metricFilter = metric.filter ? `kql='${metric.filter}'` : ''; - metricNameResolver[metric.name] = `${ - AggMappingForLens[metric.aggType] ? AggMappingForLens[metric.aggType] : metric.aggType - }(${metric.field ? metric.field : metricFilter})`; + if (metric.aggType === 'rate') { + metricNameResolver[metric.name] = `counter_rate(max(${ + metric.field ? metric.field : metricFilter + }))`; + } else { + metricNameResolver[metric.name] = `${ + AggMappingForLens[metric.aggType] ? AggMappingForLens[metric.aggType] : metric.aggType + }(${metric.field ? metric.field : metricFilter})`; + } }); let equation = criterion.equation From dcd8e0c614183ae648e00979eb82123656076d16 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Tue, 22 Oct 2024 08:32:48 -0500 Subject: [PATCH 12/12] [Security Solution][Notes] - fix user filter not checking correct license in notes management page (#197149) --- .../upselling/messages/index.tsx | 8 ++ .../upselling/service/types.ts | 3 +- .../user_profiles/use_suggest_users.tsx | 21 +++-- .../notes/components/search_row.test.tsx | 33 ++++---- .../public/notes/components/search_row.tsx | 44 ++--------- .../components/user_filter_dropdown.test.tsx | 69 ++++++++++++++++ .../notes/components/user_filter_dropdown.tsx | 79 +++++++++++++++++++ .../public/upselling/register_upsellings.tsx | 6 ++ 8 files changed, 202 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.test.tsx create mode 100644 x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx diff --git a/x-pack/packages/security-solution/upselling/messages/index.tsx b/x-pack/packages/security-solution/upselling/messages/index.tsx index 722a711995d01..4bda9477f13c0 100644 --- a/x-pack/packages/security-solution/upselling/messages/index.tsx +++ b/x-pack/packages/security-solution/upselling/messages/index.tsx @@ -46,3 +46,11 @@ export const ALERT_SUPPRESSION_RULE_DETAILS = i18n.translate( 'Alert suppression is configured but will not be applied due to insufficient licensing', } ); + +export const UPGRADE_NOTES_MANAGEMENT_USER_FILTER = (requiredLicense: string) => + i18n.translate('securitySolutionPackages.noteManagement.userFilter.upsell', { + defaultMessage: 'Upgrade to {requiredLicense} to make use of user filters', + values: { + requiredLicense, + }, + }); diff --git a/x-pack/packages/security-solution/upselling/service/types.ts b/x-pack/packages/security-solution/upselling/service/types.ts index 43019271a7e02..b053c9aedf857 100644 --- a/x-pack/packages/security-solution/upselling/service/types.ts +++ b/x-pack/packages/security-solution/upselling/service/types.ts @@ -27,4 +27,5 @@ export type UpsellingMessageId = | 'investigation_guide_interactions' | 'alert_assignments' | 'alert_suppression_rule_form' - | 'alert_suppression_rule_details'; + | 'alert_suppression_rule_details' + | 'note_management_user_filter'; diff --git a/x-pack/plugins/security_solution/public/common/components/user_profiles/use_suggest_users.tsx b/x-pack/plugins/security_solution/public/common/components/user_profiles/use_suggest_users.tsx index a8a2338e51e9d..626d621f61a30 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_profiles/use_suggest_users.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_profiles/use_suggest_users.tsx @@ -13,10 +13,6 @@ import { suggestUsers } from './api'; import { USER_PROFILES_FAILURE } from './translations'; import { useAppToasts } from '../../hooks/use_app_toasts'; -export interface SuggestUserProfilesArgs { - searchTerm: string; -} - export const bulkGetUserProfiles = async ({ searchTerm, }: { @@ -25,7 +21,21 @@ export const bulkGetUserProfiles = async ({ return suggestUsers({ searchTerm }); }; -export const useSuggestUsers = ({ searchTerm }: { searchTerm: string }) => { +export interface UseSuggestUsersParams { + /** + * Search term to filter user profiles + */ + searchTerm: string; + /** + * Whether the query should be enabled + */ + enabled?: boolean; +} + +/** + * Fetches user profiles based on a search term + */ +export const useSuggestUsers = ({ enabled = true, searchTerm }: UseSuggestUsersParams) => { const { addError } = useAppToasts(); return useQuery( @@ -36,6 +46,7 @@ export const useSuggestUsers = ({ searchTerm }: { searchTerm: string }) => { { retry: false, staleTime: Infinity, + enabled, onError: (e) => { addError(e, { title: USER_PROFILES_FAILURE }); }, diff --git a/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx b/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx index be9546c77525b..447ade158306b 100644 --- a/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import { fireEvent, render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { SearchRow } from './search_row'; import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids'; import { AssociatedFilter } from '../../../common/notes/constants'; import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users'; +import { TestProviders } from '../../common/mock'; jest.mock('../../common/components/user_profiles/use_suggest_users'); @@ -35,7 +36,11 @@ describe('SearchRow', () => { }); it('should render the component', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument(); expect(getByTestId(USER_SELECT_TEST_ID)).toBeInTheDocument(); @@ -43,7 +48,11 @@ describe('SearchRow', () => { }); it('should call the correct action when entering a value in the search bar', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); const searchBox = getByTestId(SEARCH_BAR_TEST_ID); @@ -53,20 +62,12 @@ describe('SearchRow', () => { expect(mockDispatch).toHaveBeenCalled(); }); - it('should call the correct action when select a user', async () => { - const { getByTestId } = render(); - - const userSelect = getByTestId('comboBoxSearchInput'); - userSelect.focus(); - - const option = await screen.findByText('test'); - fireEvent.click(option); - - expect(mockDispatch).toHaveBeenCalled(); - }); - it('should call the correct action when select a value in the associated note dropdown', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); const associatedNoteSelect = getByTestId(ASSOCIATED_NOT_SELECT_TEST_ID); await userEvent.selectOptions(associatedNoteSelect, [AssociatedFilter.documentOnly]); diff --git a/x-pack/plugins/security_solution/public/notes/components/search_row.tsx b/x-pack/plugins/security_solution/public/notes/components/search_row.tsx index d540a586814d8..f2f90b3ba7e0d 100644 --- a/x-pack/plugins/security_solution/public/notes/components/search_row.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/search_row.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React, { useMemo, useCallback, useState } from 'react'; +import React, { useCallback } from 'react'; import type { EuiSelectOption } from '@elastic/eui'; import { - EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiSearchBar, @@ -16,17 +15,12 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import { useDispatch } from 'react-redux'; -import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; import { i18n } from '@kbn/i18n'; -import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; -import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids'; -import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users'; -import { userFilterAssociatedNotes, userFilterUsers, userSearchedNotes } from '..'; +import { UserFilterDropdown } from './user_filter_dropdown'; +import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID } from './test_ids'; +import { userFilterAssociatedNotes, userSearchedNotes } from '..'; import { AssociatedFilter } from '../../../common/notes/constants'; -export const USERS_DROPDOWN = i18n.translate('xpack.securitySolution.notes.usersDropdownLabel', { - defaultMessage: 'Users', -}); const FILTER_SELECT = i18n.translate('xpack.securitySolution.notes.management.filterSelect', { defaultMessage: 'Select filter', }); @@ -55,26 +49,6 @@ export const SearchRow = React.memo(() => { [dispatch] ); - const { isLoading: isLoadingSuggestedUsers, data: userProfiles } = useSuggestUsers({ - searchTerm: '', - }); - const users = useMemo( - () => - (userProfiles || []).map((userProfile: UserProfileWithAvatar) => ({ - label: userProfile.user.full_name || userProfile.user.username, - })), - [userProfiles] - ); - - const [selectedUser, setSelectedUser] = useState>>(); - const onChange = useCallback( - (user: Array>) => { - setSelectedUser(user); - dispatch(userFilterUsers(user.length > 0 ? user[0].label : '')); - }, - [dispatch] - ); - const onAssociatedNoteSelectChange = useCallback( (e: React.ChangeEvent) => { dispatch(userFilterAssociatedNotes(e.target.value as AssociatedFilter)); @@ -88,15 +62,7 @@ export const SearchRow = React.memo(() => {
- + { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +describe('UserFilterDropdown', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useSuggestUsers as jest.Mock).mockReturnValue({ + isLoading: false, + data: [{ user: { username: 'test' } }, { user: { username: 'elastic' } }], + }); + (useLicense as jest.Mock).mockReturnValue({ isPlatinumPlus: () => true }); + (useUpsellingMessage as jest.Mock).mockReturnValue('upsellingMessage'); + }); + + it('should render the component enabled', () => { + const { getByTestId } = render(); + + const dropdown = getByTestId(USER_SELECT_TEST_ID); + + expect(dropdown).toBeInTheDocument(); + expect(dropdown).not.toHaveClass('euiComboBox-isDisabled'); + }); + + it('should render the dropdown disabled', async () => { + (useLicense as jest.Mock).mockReturnValue({ isPlatinumPlus: () => false }); + + const { getByTestId } = render(); + + expect(getByTestId(USER_SELECT_TEST_ID)).toHaveClass('euiComboBox-isDisabled'); + }); + + it('should call the correct action when select a user', async () => { + const { getByTestId } = render(); + + const userSelect = getByTestId('comboBoxSearchInput'); + userSelect.focus(); + + const option = await screen.findByText('test'); + fireEvent.click(option); + + expect(mockDispatch).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx b/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx new file mode 100644 index 0000000000000..78f4ef6dd2ac8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx @@ -0,0 +1,79 @@ +/* + * 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, { useMemo, useCallback, useState } from 'react'; +import { EuiComboBox, EuiToolTip } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { i18n } from '@kbn/i18n'; +import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; +import { useLicense } from '../../common/hooks/use_license'; +import { useUpsellingMessage } from '../../common/hooks/use_upselling'; +import { USER_SELECT_TEST_ID } from './test_ids'; +import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users'; +import { userFilterUsers } from '..'; + +export const USERS_DROPDOWN = i18n.translate('xpack.securitySolution.notes.usersDropdownLabel', { + defaultMessage: 'Users', +}); + +export const UserFilterDropdown = React.memo(() => { + const dispatch = useDispatch(); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const upsellingMessage = useUpsellingMessage('note_management_user_filter'); + + const { isLoading, data } = useSuggestUsers({ + searchTerm: '', + enabled: isPlatinumPlus, + }); + const users = useMemo( + () => + (data || []).map((userProfile: UserProfileWithAvatar) => ({ + label: userProfile.user.full_name || userProfile.user.username, + })), + [data] + ); + + const [selectedUser, setSelectedUser] = useState>>(); + const onChange = useCallback( + (user: Array>) => { + setSelectedUser(user); + dispatch(userFilterUsers(user.length > 0 ? user[0].label : '')); + }, + [dispatch] + ); + + const dropdown = useMemo( + () => ( + + ), + [isLoading, isPlatinumPlus, onChange, selectedUser, users] + ); + + return ( + <> + {isPlatinumPlus ? ( + <>{dropdown} + ) : ( + + {dropdown} + + )} + + ); +}); + +UserFilterDropdown.displayName = 'UserFilterDropdown'; diff --git a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx index b7fbdab3b5982..69f3c5dd4cc28 100644 --- a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx @@ -12,6 +12,7 @@ import { ALERT_SUPPRESSION_RULE_FORM, UPGRADE_ALERT_ASSIGNMENTS, UPGRADE_INVESTIGATION_GUIDE, + UPGRADE_NOTES_MANAGEMENT_USER_FILTER, } from '@kbn/security-solution-upselling/messages'; import type { MessageUpsellings, @@ -132,4 +133,9 @@ export const upsellingMessages: UpsellingMessages = [ minimumLicenseRequired: 'platinum', message: ALERT_SUPPRESSION_RULE_DETAILS, }, + { + id: 'note_management_user_filter', + minimumLicenseRequired: 'platinum', + message: UPGRADE_NOTES_MANAGEMENT_USER_FILTER('Platinum'), + }, ];