From 37b42a2cc6184874c42e1c4e8605487a3f5cc248 Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Mon, 18 Sep 2023 13:02:45 -0300 Subject: [PATCH 01/20] Added plugins dependencies --- plugins/main/opensearch_dashboards.json | 2 ++ plugins/main/public/types.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/main/opensearch_dashboards.json b/plugins/main/opensearch_dashboards.json index 8fc3ca537f..8fa3348d07 100644 --- a/plugins/main/opensearch_dashboards.json +++ b/plugins/main/opensearch_dashboards.json @@ -6,6 +6,8 @@ "requiredPlugins": [ "navigation", "data", + "dashboard", + "embeddable", "discover", "inspector", "visualizations", diff --git a/plugins/main/public/types.ts b/plugins/main/public/types.ts index 3a75c7fdc6..9a62af2de6 100644 --- a/plugins/main/public/types.ts +++ b/plugins/main/public/types.ts @@ -8,16 +8,18 @@ import { UiActionsSetup } from '../../../src/plugins/ui_actions/public'; import { SecurityOssPluginStart } from '../../../src/plugins/security_oss/public/'; import { SavedObjectsStart } from '../../../src/plugins/saved_objects/public'; import { TelemetryPluginStart, TelemetryPluginSetup } from '../../../src/plugins/telemetry/public'; +import { DashboardStart } from '../../../src/plugins/dashboard/public' export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; data: DataPublicPluginStart; visualizations: VisualizationsStart; discover: DiscoverStart; - charts: ChartsPluginStart - securityOss: SecurityOssPluginStart, - savedObjects: SavedObjectsStart, - telemetry: TelemetryPluginStart + charts: ChartsPluginStart; + securityOss: SecurityOssPluginStart; + savedObjects: SavedObjectsStart; + telemetry: TelemetryPluginStart; + dashboard: DashboardStart; } export interface AppDependencies { core: CoreStart; From 9a2ff52238f0f9c594f825f855f45018df94d489 Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Mon, 18 Sep 2023 13:03:32 -0300 Subject: [PATCH 02/20] Add new inventories tabs scaffolding --- .../components/common/modules/modules-defaults.js | 13 ++++++++++--- .../vulnerabilities/dashboard/dashboard.tsx | 7 +++++++ .../overview/vulnerabilities/dashboard/index.tsx | 1 + .../components/overview/vulnerabilities/index.tsx | 2 ++ .../overview/vulnerabilities/inventory/index.tsx | 1 + .../vulnerabilities/inventory/inventory.tsx | 7 +++++++ 6 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboard/dashboard.tsx create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboard/index.tsx create mode 100644 plugins/main/public/components/overview/vulnerabilities/index.tsx create mode 100644 plugins/main/public/components/overview/vulnerabilities/inventory/index.tsx create mode 100644 plugins/main/public/components/overview/vulnerabilities/inventory/inventory.tsx diff --git a/plugins/main/public/components/common/modules/modules-defaults.js b/plugins/main/public/components/common/modules/modules-defaults.js index 0b74074259..956a396495 100644 --- a/plugins/main/public/components/common/modules/modules-defaults.js +++ b/plugins/main/public/components/common/modules/modules-defaults.js @@ -21,7 +21,8 @@ import ButtonModuleExploreAgent from '../../../controllers/overview/components/o import { ButtonModuleGenerateReport } from '../modules/buttons'; import { OfficePanel } from '../../overview/office-panel'; import { GitHubPanel } from '../../overview/github-panel'; -import { withModuleNotForAgent } from '../hocs'; +import { DashboardVuls, InventoryVuls } from '../../overview/vulnerabilities' +import { withModuleNotForAgent, withModuleTabLoader } from '../hocs'; const DashboardTab = { id: 'dashboard', @@ -130,13 +131,19 @@ export const ModulesDefaults = { availableFor: ['manager', 'agent'], }, vuls: { - init: 'inventory', + init: 'dashboard', tabs: [ + { + id: 'dashboard', + name: 'Dashboard', + buttons: [ButtonModuleExploreAgent], + component: withModuleNotForAgent(DashboardVuls), + }, { id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], - component: MainVuls, + component: withModuleNotForAgent(InventoryVuls), }, EventsTab, ], diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboard/dashboard.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboard/dashboard.tsx new file mode 100644 index 0000000000..db923f1eb0 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboard/dashboard.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const DashboardVuls = () => { + return ( +
Vulnerabilities Dashboard
+ ); +} diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboard/index.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboard/index.tsx new file mode 100644 index 0000000000..b691822976 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboard/index.tsx @@ -0,0 +1 @@ +export * from './dashboard'; \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/index.tsx b/plugins/main/public/components/overview/vulnerabilities/index.tsx new file mode 100644 index 0000000000..39628aef4e --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/index.tsx @@ -0,0 +1,2 @@ +export * from './dashboard'; +export * from './inventory'; \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/inventory/index.tsx b/plugins/main/public/components/overview/vulnerabilities/inventory/index.tsx new file mode 100644 index 0000000000..ddb0742f5e --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/inventory/index.tsx @@ -0,0 +1 @@ +export * from './inventory'; \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/inventory/inventory.tsx b/plugins/main/public/components/overview/vulnerabilities/inventory/inventory.tsx new file mode 100644 index 0000000000..b94a76b6c7 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/inventory/inventory.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const InventoryVuls = () => { + return ( +
Inventory dashboard
+ ); +} From 5d5333da65bd7c32f1c5702e578a502e9f5e455f Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Wed, 27 Sep 2023 11:06:48 -0300 Subject: [PATCH 03/20] Refactor arquitecture --- .../components/overview/vulnerabilities/dashboards/index.ts | 2 ++ .../vulnerabilities/{ => dashboards}/inventory/index.tsx | 0 .../vulnerabilities/{ => dashboards}/inventory/inventory.tsx | 0 .../{dashboard => dashboards/overview}/dashboard.tsx | 0 .../{dashboard => dashboards/overview}/index.tsx | 0 .../main/public/components/overview/vulnerabilities/index.tsx | 3 +-- 6 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/index.ts rename plugins/main/public/components/overview/vulnerabilities/{ => dashboards}/inventory/index.tsx (100%) rename plugins/main/public/components/overview/vulnerabilities/{ => dashboards}/inventory/inventory.tsx (100%) rename plugins/main/public/components/overview/vulnerabilities/{dashboard => dashboards/overview}/dashboard.tsx (100%) rename plugins/main/public/components/overview/vulnerabilities/{dashboard => dashboards/overview}/index.tsx (100%) diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/index.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/index.ts new file mode 100644 index 0000000000..234af15583 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/index.ts @@ -0,0 +1,2 @@ +export * from './overview'; +export * from './inventory'; \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/inventory/index.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/index.tsx similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/inventory/index.tsx rename to plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/index.tsx diff --git a/plugins/main/public/components/overview/vulnerabilities/inventory/inventory.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/inventory/inventory.tsx rename to plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboard/dashboard.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/dashboard/dashboard.tsx rename to plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboard/index.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/index.tsx similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/dashboard/index.tsx rename to plugins/main/public/components/overview/vulnerabilities/dashboards/overview/index.tsx diff --git a/plugins/main/public/components/overview/vulnerabilities/index.tsx b/plugins/main/public/components/overview/vulnerabilities/index.tsx index 39628aef4e..d46e147b8e 100644 --- a/plugins/main/public/components/overview/vulnerabilities/index.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/index.tsx @@ -1,2 +1 @@ -export * from './dashboard'; -export * from './inventory'; \ No newline at end of file +export * from './dashboards'; \ No newline at end of file From 350d7f70d2cee728f0e3c7f9075c99ef63967695 Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Wed, 27 Sep 2023 11:09:12 -0300 Subject: [PATCH 04/20] Add searchbar folder --- .../components/overview/vulnerabilities/searchbar/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 plugins/main/public/components/overview/vulnerabilities/searchbar/index.ts diff --git a/plugins/main/public/components/overview/vulnerabilities/searchbar/index.ts b/plugins/main/public/components/overview/vulnerabilities/searchbar/index.ts new file mode 100644 index 0000000000..22509f1244 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/searchbar/index.ts @@ -0,0 +1 @@ +// searchbar index \ No newline at end of file From 2ccf4cfb05f607db775e88d6bbd359612a408715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Biset?= <43619595+jbiset@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:09:15 -0300 Subject: [PATCH 05/20] Add useDashboardConfiguration hook for configuring vulnerability dashboards (#5947) * Add useDashboardConfiguration and unit test hook * Changed how the initial hook configuration is set --- .../hooks/use-dashboard-configuration.test.ts | 69 +++++++++++++++++++ .../hooks/use-dashboard-configuration.tsx | 33 +++++++++ 2 files changed, 102 insertions(+) create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.test.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.tsx diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.test.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.test.ts new file mode 100644 index 0000000000..f77c25e9b7 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.test.ts @@ -0,0 +1,69 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; +import { DashboardContainerInput } from '../../../../../../../../src/plugins/dashboard/public'; +import { useDashboardConfiguration } from './use-dashboard-configuration'; + +describe('useDashboardConfiguration', () => { + test('initial configuration is set correctly', () => { + const emptyInitialConfiguration: DashboardContainerInput = { + viewMode: ViewMode.EDIT, + filters: [], + query: undefined, + timeRange: undefined, + useMargins: false, + title: '', + isFullScreenMode: false, + panels: {}, + id: '', + }; + + const { result } = renderHook(() => useDashboardConfiguration()); + + expect(result.current.configuration).toEqual(emptyInitialConfiguration); + expect(typeof result.current.updateConfiguration).toBe('function'); + }); + + test('updateConfiguration updates configuration correctly', () => { + const { result } = renderHook(() => useDashboardConfiguration()); + + const updatedConfig = { + viewMode: ViewMode.VIEW, + title: 'Updated Title', + }; + + act(() => { + result.current.updateConfiguration(updatedConfig); + }); + + expect(result.current.configuration.viewMode).toBe(updatedConfig.viewMode); + expect(result.current.configuration.title).toBe(updatedConfig.title); + }); + + test('updateConfiguration merges properties correctly', () => { + const { result } = renderHook(() => useDashboardConfiguration()); + + const updatedConfig = { + viewMode: ViewMode.VIEW, + title: 'Updated Title', + }; + + act(() => { + result.current.updateConfiguration(updatedConfig); + }); + + expect(result.current.configuration.viewMode).toBe(updatedConfig.viewMode); + expect(result.current.configuration.title).toBe(updatedConfig.title); + + const additionalUpdate = { + description: 'Updated Description', + }; + + act(() => { + result.current.updateConfiguration(additionalUpdate); + }); + + expect(result.current.configuration.viewMode).toBe(updatedConfig.viewMode); + expect(result.current.configuration.title).toBe(updatedConfig.title); + expect(result.current.configuration.description).toBe(additionalUpdate.description); + }); +}); diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.tsx new file mode 100644 index 0000000000..bed04ebb19 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import { DashboardContainerInput } from '../../../../../../../../src/plugins/dashboard/public'; +import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; + +const emptyInitialConfiguration: DashboardContainerInput = { + viewMode: ViewMode.EDIT, + filters: [], + query: undefined, + timeRange: undefined, + useMargins: false, + title: '', + isFullScreenMode: false, + panels: {}, + id: '', +}; + +export const useDashboardConfiguration = (initialConfiguration?: DashboardContainerInput) => { + const [configuration, setConfiguration] = useState( + initialConfiguration ?? emptyInitialConfiguration + ); + + const updateConfiguration = (updatedConfig: Partial) => { + setConfiguration((prevConfig: DashboardContainerInput) => ({ + ...prevConfig, + ...updatedConfig, + })); + }; + + return { + configuration, + updateConfiguration, + }; +}; From 8b76f24d0219cf813073c3c10ae713669cbeccf3 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Date: Fri, 29 Sep 2023 18:49:16 -0300 Subject: [PATCH 06/20] =?UTF-8?q?[Vulnerabilities=20dashboards]=C2=A0Add?= =?UTF-8?q?=20search=20bar=20services=20(#5960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add search bar hooks * Rename searchbar hook * Add unit test to use search bar configuration hook * Fix some unit test titles * Remove console.log * Solve requested changes * Fix request changes and hook filename --- .../common/hooks/use-filter-manager.ts | 8 +- .../vulnerabilities/common/constants.ts | 1 + .../use-search-bar-configuration.test.ts | 219 ++++++++++++++++++ .../use-search-bar-configuration.tsx | 127 ++++++++++ 4 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 plugins/main/public/components/overview/vulnerabilities/common/constants.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.test.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx diff --git a/plugins/main/public/components/common/hooks/use-filter-manager.ts b/plugins/main/public/components/common/hooks/use-filter-manager.ts index c9bad4d019..884279668c 100644 --- a/plugins/main/public/components/common/hooks/use-filter-manager.ts +++ b/plugins/main/public/components/common/hooks/use-filter-manager.ts @@ -13,8 +13,14 @@ import { getDataPlugin } from '../../../kibana-services'; import { useState, useEffect, useMemo } from 'react'; import { Filter } from 'src/plugins/data/public'; import _ from 'lodash'; +import { FilterManager } from '../../../../../../src/plugins/data/public'; -export const useFilterManager = () => { +type tUseFilterManagerReturn = { + filterManager: FilterManager; + filters: Filter[]; +} + +export const useFilterManager = () : tUseFilterManagerReturn => { const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); const [filters, setFilters] = useState(filterManager.getFilters()); diff --git a/plugins/main/public/components/overview/vulnerabilities/common/constants.ts b/plugins/main/public/components/overview/vulnerabilities/common/constants.ts new file mode 100644 index 0000000000..665c99285a --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/common/constants.ts @@ -0,0 +1 @@ +export const VULNERABILITIES_INDEX_PATTERN_ID = 'wazuh-inventory-cve' \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.test.ts b/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.test.ts new file mode 100644 index 0000000000..0de91e5bab --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.test.ts @@ -0,0 +1,219 @@ +import { renderHook } from '@testing-library/react-hooks'; +import '@testing-library/jest-dom/extend-expect'; +// osd dependencies +import { Start, dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { + Filter, + IndexPattern, + Query, + TimeRange, +} from '../../../../../../../src/plugins/data/public'; +// wazuh plugin dependencies +import useSearchBar from './use-search-bar-configuration'; +import { getDataPlugin } from '../../../../kibana-services'; +import * as timeFilterHook from '../../../common/hooks/use-time-filter'; +import * as queryManagerHook from '../../../common/hooks/use-query'; + +/** + * Mocking Data Plugin + **/ +jest.mock('../../../../kibana-services', () => { + return { + getDataPlugin: jest.fn(), + }; +}); +/* using osd mock utils */ +const mockDataPlugin = dataPluginMock.createStartContract(); +const mockedGetDataPlugin = getDataPlugin as jest.Mock; +mockedGetDataPlugin.mockImplementation( + () => + ({ + ...mockDataPlugin, + ...{ + query: { + ...mockDataPlugin.query, + queryString: { + ...mockDataPlugin.query.queryString, + getUpdates$: jest.fn(() => ({ + subscribe: jest.fn(), + unsubscribe: jest.fn(), + })), + }, + }, + }, + } as Start) +); +/////////////////////////////////////////////////////////// + +const mockedDefaultIndexPatternData: Partial = { + // used partial not avoid fill all the interface, it's only for testing purpose + id: 'default-index-pattern', + title: '', +}; + +describe('[hook] useSearchBarConfiguration', () => { + beforeAll(() => { + /***** mock use-time-filter hook *****/ + const spyUseTimeFilter = jest.spyOn(timeFilterHook, 'useTimeFilter'); + const mockTimeFilterResult: TimeRange = { + from: 'now/d', + to: 'now/d', + }; + spyUseTimeFilter.mockImplementation(() => ({ + timeFilter: mockTimeFilterResult, + setTimeFilter: jest.fn(), + timeHistory: [], + })); + /***** mock use-time-filter hook *****/ + const spyUseQueryManager = jest.spyOn(queryManagerHook, 'useQueryManager'); + const mockQueryResult: Query = { + language: 'kuery', + query: '', + }; + spyUseQueryManager.mockImplementation(() => [mockQueryResult, jest.fn()]); + }); + + it('should return default app index pattern when not receiving a default index pattern', async () => { + jest + .spyOn(mockDataPlugin.indexPatterns, 'getDefault') + .mockResolvedValue(mockedDefaultIndexPatternData); + jest.spyOn(mockDataPlugin.query.filterManager, 'getFilters').mockReturnValue([]); + const { result, waitForNextUpdate } = renderHook(() => useSearchBar({})); + await waitForNextUpdate(); + expect(mockDataPlugin.indexPatterns.getDefault).toBeCalled(); + expect(result.current.searchBarProps.indexPatterns).toMatchObject([ + mockedDefaultIndexPatternData, + ]); + }); + + it('should return the same index pattern when receiving a default index pattern', async () => { + const exampleIndexPatternId = 'wazuh-index-pattern'; + const mockedIndexPatternData: Partial = { + // used partial not avoid fill all the interface, it's only for testing purpose + id: exampleIndexPatternId, + title: '', + }; + jest.spyOn(mockDataPlugin.indexPatterns, 'get').mockResolvedValue(mockedIndexPatternData); + const { result, waitForNextUpdate } = renderHook(() => + useSearchBar({ + defaultIndexPatternID: 'wazuh-index-pattern', + }) + ); + await waitForNextUpdate(); + expect(mockDataPlugin.indexPatterns.get).toBeCalledWith(exampleIndexPatternId); + expect(result.current.searchBarProps.indexPatterns).toMatchObject([mockedIndexPatternData]); + }); + + it('should show an ERROR message and get the default app index pattern when not found the index pattern data by the ID received', async () => { + const INDEX_NOT_FOUND_ERROR = new Error('Index Pattern not found'); + jest.spyOn(mockDataPlugin.indexPatterns, 'get').mockImplementation(() => { + throw INDEX_NOT_FOUND_ERROR; + }); + jest + .spyOn(mockDataPlugin.indexPatterns, 'getDefault') + .mockResolvedValue(mockedDefaultIndexPatternData); + jest.spyOn(mockDataPlugin.query.filterManager, 'getFilters').mockReturnValue([]); + + // mocking console error to avoid logs in test and check if is called + const mockedConsoleError = jest.spyOn(console, 'error').mockImplementationOnce(() => {}); + const { result, waitForNextUpdate } = renderHook(() => + useSearchBar({ + defaultIndexPatternID: 'invalid-index-pattern-id', + }) + ); + + await waitForNextUpdate(); + expect(mockDataPlugin.indexPatterns.getDefault).toBeCalled(); + expect(mockDataPlugin.indexPatterns.get).toBeCalledWith('invalid-index-pattern-id'); + expect(result.current.searchBarProps.indexPatterns).toMatchObject([ + mockedDefaultIndexPatternData, + ]); + expect(mockedConsoleError).toBeCalledWith(INDEX_NOT_FOUND_ERROR); + }); + + it('should return the same filters and apply them to the filter manager when are received by props', async () => { + const defaultFilters: Filter[] = [ + { + query: 'something to filter', + meta: { + alias: 'filter-mocked', + disabled: false, + negate: true, + }, + }, + ]; + jest + .spyOn(mockDataPlugin.indexPatterns, 'getDefault') + .mockResolvedValue(mockedDefaultIndexPatternData); + jest.spyOn(mockDataPlugin.query.filterManager, 'getFilters').mockReturnValue(defaultFilters); + const { result, waitForNextUpdate } = renderHook(() => + useSearchBar({ + filters: defaultFilters, + }) + ); + + await waitForNextUpdate(); + + expect(result.current.searchBarProps.filters).toMatchObject(defaultFilters); + expect(mockDataPlugin.query.filterManager.setFilters).toBeCalledWith(defaultFilters); + expect(mockDataPlugin.query.filterManager.getFilters).toBeCalled(); + }); + + it('should return and preserve filters when the index pattern received is equal to the index pattern already selected in the app', async () => { + const defaultIndexFilters: Filter[] = [ + { + query: 'something to filter', + meta: { + alias: 'filter-mocked', + disabled: false, + negate: true, + }, + }, + ]; + jest + .spyOn(mockDataPlugin.indexPatterns, 'getDefault') + .mockResolvedValue(mockedDefaultIndexPatternData); + jest + .spyOn(mockDataPlugin.indexPatterns, 'get') + .mockResolvedValue(mockedDefaultIndexPatternData); + jest + .spyOn(mockDataPlugin.query.filterManager, 'getFilters') + .mockReturnValue(defaultIndexFilters); + const { result, waitForNextUpdate } = renderHook(() => + useSearchBar({ + defaultIndexPatternID: mockedDefaultIndexPatternData.id, + }) + ); + await waitForNextUpdate(); + expect(result.current.searchBarProps.indexPatterns).toMatchObject([ + mockedDefaultIndexPatternData, + ]); + expect(result.current.searchBarProps.filters).toMatchObject(defaultIndexFilters); + }); + + it('should return empty filters when the index pattern is NOT equal to the default app index pattern', async () => { + const exampleIndexPatternId = 'wazuh-index-pattern'; + const mockedExampleIndexPatternData: Partial = { + // used partial not avoid fill all the interface, it's only for testing purpose + id: exampleIndexPatternId, + title: '', + }; + jest + .spyOn(mockDataPlugin.indexPatterns, 'get') + .mockResolvedValue(mockedExampleIndexPatternData); + jest + .spyOn(mockDataPlugin.indexPatterns, 'getDefault') + .mockResolvedValue(mockedDefaultIndexPatternData); + jest.spyOn(mockDataPlugin.query.filterManager, 'getFilters').mockReturnValue([]); + const { result, waitForNextUpdate } = renderHook(() => + useSearchBar({ + defaultIndexPatternID: exampleIndexPatternId, + }) + ); + await waitForNextUpdate(); + expect(result.current.searchBarProps.indexPatterns).toMatchObject([ + mockedExampleIndexPatternData, + ]); + expect(result.current.searchBarProps.filters).toStrictEqual([]); + }); +}); diff --git a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx b/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx new file mode 100644 index 0000000000..da42e8ae52 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx @@ -0,0 +1,127 @@ +import React, { useEffect, useState } from 'react' +import { SearchBarProps, FilterManager, TimeRange, Query } from '../../../../../../../src/plugins/data/public' +import { Filter, IIndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; +import { getDataPlugin } from '../../../../kibana-services'; + +import { useFilterManager, useQueryManager, useTimeFilter } from '../../../common/hooks'; +import { AUTHORIZED_AGENTS } from '../../../../../common/constants'; + +// Input - types +type tUseSearchBarCustomInputs = { + defaultIndexPatternID?: IIndexPattern['id']; + onFiltersUpdated?: (filters: Filter[]) => void; + onQuerySubmitted?: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; +} +type tUseSearchBarProps = Partial & tUseSearchBarCustomInputs; + +// Output types +type tUserSearchBarResponse = { + searchBarProps: Partial; +} + +/** + * Hook used to compose the searchbar configuration props + * @param props + * @returns + */ +const useSearchBarConfiguration = (props?: tUseSearchBarProps): tUserSearchBarResponse => { + // dependencies + const filterManager = useFilterManager().filterManager as FilterManager; + const [query, setQuery] = props?.query ? useState(props?.query) : useQueryManager(); + const { timeFilter, timeHistory, setTimeFilter } = useTimeFilter(); + // states + const [isLoading, setIsLoading] = useState(false); + const [indexPatternSelected, setIndexPatternSelected] = useState(); + + useEffect(() => { + setIsLoading(true); + initSearchBar(); + setIsLoading(false); + }, []); + + /** + * Initialize the searchbar props with the corresponding index pattern and filters + */ + const initSearchBar = async () => { + const indexPattern = await getIndexPattern(props?.defaultIndexPatternID); + setIndexPatternSelected(indexPattern); + const filters = await getInitialFilters(indexPattern); + filterManager.setFilters(filters); + } + + /** + * Return the index pattern data by ID. + * If not receive a ID return the default index from the index pattern service + * @returns + */ + const getIndexPattern = async (indexPatternID?: string) => { + const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; + if(indexPatternID){ + try{ + return await indexPatternService.get(indexPatternID); + }catch(error){ + // when the index pattern id not exists will get the default + console.error(error); + return await indexPatternService.getDefault(); + } + }else{ + return await indexPatternService.getDefault(); + } + + } + + /** + * Return the initial filters considering if hook receives initial filters + * When the default index pattern is the same like the received preserve the filters + * @param indexPattern + * @returns + */ + const getInitialFilters = async (indexPattern: IIndexPattern) => { + const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; + let initialFilters: Filter[] = []; + if(props?.filters){ + return props?.filters; + } + if(indexPattern){ + // get filtermanager and filters + // if the index is the same, get filters stored + // else clear filters + const defaultIndexPattern = await indexPatternService.getDefault() as IIndexPattern; + initialFilters = defaultIndexPattern.id === indexPattern.id ? filterManager.getFilters(): [] + }else{ + initialFilters = []; + } + return initialFilters.filter(filter => filter.meta.controlledBy !== AUTHORIZED_AGENTS); + } + + /** + * Search bar properties necessary to render and initialize the osd search bar component + */ + const searchBarProps: Partial = { + isLoading, + ...indexPatternSelected && { indexPatterns: [indexPatternSelected] }, // indexPattern cannot be empty or empty [] + filters: filterManager.getFilters(), + query, + timeHistory, + dateRangeFrom: timeFilter.from, + dateRangeTo: timeFilter.to, + onFiltersUpdated: (filters: Filter[]) => { + // its necessary execute setter to apply filters + filterManager.setFilters(filters); + props?.onFiltersUpdated && props?.onFiltersUpdated(filters); + }, + onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }, _isUpdate?: boolean): void => { + const { dateRange, query } = payload; + // its necessary execute setter to apply query filters + setTimeFilter(dateRange); + setQuery(query); + props?.onQuerySubmitted && props?.onQuerySubmitted(payload); + } + } + + return { + searchBarProps, + } +} + +export default useSearchBarConfiguration; \ No newline at end of file From 4bacf9711a9a5019ef54cef58a80fb7e580645c9 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Date: Thu, 5 Oct 2023 17:11:55 -0300 Subject: [PATCH 07/20] [Vulnerabilities dashboards] Fix wrong agent.id filters loaded in search bar by default (#5970) Remove agent id filter in searchbar --- .../use-search-bar-configuration.tsx | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx b/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx index da42e8ae52..ee8b629263 100644 --- a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx @@ -12,7 +12,7 @@ type tUseSearchBarCustomInputs = { onFiltersUpdated?: (filters: Filter[]) => void; onQuerySubmitted?: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; } -type tUseSearchBarProps = Partial & tUseSearchBarCustomInputs; +type tUseSearchBarProps = Partial & tUseSearchBarCustomInputs; // Output types type tUserSearchBarResponse = { @@ -56,18 +56,18 @@ const useSearchBarConfiguration = (props?: tUseSearchBarProps): tUserSearchBarRe */ const getIndexPattern = async (indexPatternID?: string) => { const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; - if(indexPatternID){ - try{ + if (indexPatternID) { + try { return await indexPatternService.get(indexPatternID); - }catch(error){ + } catch (error) { // when the index pattern id not exists will get the default console.error(error); return await indexPatternService.getDefault(); } - }else{ + } else { return await indexPatternService.getDefault(); } - + } /** @@ -79,19 +79,29 @@ const useSearchBarConfiguration = (props?: tUseSearchBarProps): tUserSearchBarRe const getInitialFilters = async (indexPattern: IIndexPattern) => { const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; let initialFilters: Filter[] = []; - if(props?.filters){ + if (props?.filters) { return props?.filters; } - if(indexPattern){ + if (indexPattern) { // get filtermanager and filters // if the index is the same, get filters stored // else clear filters const defaultIndexPattern = await indexPatternService.getDefault() as IIndexPattern; - initialFilters = defaultIndexPattern.id === indexPattern.id ? filterManager.getFilters(): [] - }else{ + initialFilters = defaultIndexPattern.id === indexPattern.id ? filterManager.getFilters() : [] + } else { initialFilters = []; } - return initialFilters.filter(filter => filter.meta.controlledBy !== AUTHORIZED_AGENTS); + return initialFilters; + } + + /** + * Return filters from filters manager. + * Additionally solve the known issue with the auto loaded agent.id filters from the searchbar + * @returns + */ + const getFilters = () => { + const filters = filterManager ? filterManager.getFilters() : []; + return filters.filter(filter => filter.meta.controlledBy !== AUTHORIZED_AGENTS); // remove auto loaded agent.id filters } /** @@ -100,7 +110,7 @@ const useSearchBarConfiguration = (props?: tUseSearchBarProps): tUserSearchBarRe const searchBarProps: Partial = { isLoading, ...indexPatternSelected && { indexPatterns: [indexPatternSelected] }, // indexPattern cannot be empty or empty [] - filters: filterManager.getFilters(), + filters: getFilters(), query, timeHistory, dateRangeFrom: timeFilter.from, From f083c9515563fbcd5b512752ad29fdfc37c56c9d Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:12:47 -0300 Subject: [PATCH 08/20] Change new vulnerabilities inventory table (#6047) * Add data grid hook * Add doc viewer component and hook * Add ui utils components * Add new vuls inventory component * Add vuls inventory in module rendering * Add full height container * Add inventory table columns * Remove columns fields filter by keyword type --- .../common/modules/modules-defaults.js | 2 +- .../common/components/loading_spinner.scss | 4 + .../common/components/loading_spinner.tsx | 21 ++ .../common/components/no_results.tsx | 198 ++++++++++++++++++ .../vulnerabilities/common/constants.ts | 2 +- .../dashboards/inventory/config/index.ts | 40 ++++ .../dashboards/inventory/inventory.scss | 8 + .../dashboards/inventory/inventory.tsx | 165 ++++++++++++++- .../data_grid/use_data_grid.ts | 137 ++++++++++++ .../vulnerabilities/doc_viewer/doc_viewer.tsx | 150 +++++++++++++ .../doc_viewer/use_doc_viewer.ts | 28 +++ 11 files changed, 751 insertions(+), 4 deletions(-) create mode 100644 plugins/main/public/components/overview/vulnerabilities/common/components/loading_spinner.scss create mode 100644 plugins/main/public/components/overview/vulnerabilities/common/components/loading_spinner.tsx create mode 100644 plugins/main/public/components/overview/vulnerabilities/common/components/no_results.tsx create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.scss create mode 100644 plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/doc_viewer/doc_viewer.tsx create mode 100644 plugins/main/public/components/overview/vulnerabilities/doc_viewer/use_doc_viewer.ts diff --git a/plugins/main/public/components/common/modules/modules-defaults.js b/plugins/main/public/components/common/modules/modules-defaults.js index 956a396495..75838b8d9b 100644 --- a/plugins/main/public/components/common/modules/modules-defaults.js +++ b/plugins/main/public/components/common/modules/modules-defaults.js @@ -148,7 +148,7 @@ export const ModulesDefaults = { EventsTab, ], buttons: ['settings'], - availableFor: ['manager', 'agent'], + availableFor: ['manager'], }, mitre: { init: 'dashboard', diff --git a/plugins/main/public/components/overview/vulnerabilities/common/components/loading_spinner.scss b/plugins/main/public/components/overview/vulnerabilities/common/components/loading_spinner.scss new file mode 100644 index 0000000000..051ab642c1 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/common/components/loading_spinner.scss @@ -0,0 +1,4 @@ +.discoverNoResults { + display: flex; + align-items: center; +} diff --git a/plugins/main/public/components/overview/vulnerabilities/common/components/loading_spinner.tsx b/plugins/main/public/components/overview/vulnerabilities/common/components/loading_spinner.tsx new file mode 100644 index 0000000000..7f505e6167 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/common/components/loading_spinner.tsx @@ -0,0 +1,21 @@ +import './loading_spinner.scss'; +import React from 'react'; +import { EuiTitle, EuiPanel, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; + +export function LoadingSpinner() { + return ( + + } + title={ + +

+ +

+
+ } + /> +
+ ); +} diff --git a/plugins/main/public/components/overview/vulnerabilities/common/components/no_results.tsx b/plugins/main/public/components/overview/vulnerabilities/common/components/no_results.tsx new file mode 100644 index 0000000000..3d592c867d --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/common/components/no_results.tsx @@ -0,0 +1,198 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; + +import { + EuiCallOut, + EuiCode, + EuiDescriptionList, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +interface Props { + timeFieldName?: string; + queryLanguage?: string; +} + +export const DiscoverNoResults = ({ timeFieldName, queryLanguage }: Props) => { + let timeFieldMessage; + + if (timeFieldName) { + timeFieldMessage = ( + + + + +

+ +

+ +

+ +

+
+
+ ); + } + + let luceneQueryMessage; + + if (queryLanguage === 'lucene') { + const searchExamples = [ + { + description: 200, + title: ( + + + + + + ), + }, + { + description: status:200, + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499], + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499] AND extension:PHP, + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499] AND (extension:php OR extension:html), + title: ( + + + + + + ), + }, + ]; + + luceneQueryMessage = ( + + + + +

+ +

+ +

+ +

+
+ + + + + + +
+ ); + } + + return ( + + + + } + color="warning" + iconType="help" + data-test-subj="discoverNoResults" + /> + {timeFieldMessage} + {luceneQueryMessage} + + + ); +}; diff --git a/plugins/main/public/components/overview/vulnerabilities/common/constants.ts b/plugins/main/public/components/overview/vulnerabilities/common/constants.ts index 665c99285a..29536bb7f2 100644 --- a/plugins/main/public/components/overview/vulnerabilities/common/constants.ts +++ b/plugins/main/public/components/overview/vulnerabilities/common/constants.ts @@ -1 +1 @@ -export const VULNERABILITIES_INDEX_PATTERN_ID = 'wazuh-inventory-cve' \ No newline at end of file +export const VULNERABILITIES_INDEX_PATTERN_ID = 'wazuh-states-vulnerabilities'; \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts new file mode 100644 index 0000000000..fce980ecad --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts @@ -0,0 +1,40 @@ +import { EuiDataGridColumn } from "@elastic/eui"; + +export const inventoryTableDefaultColumns: EuiDataGridColumn[] = [ + { + id: '@timestamp', + displayAsText: 'Timestamp', + }, + { + id: 'package.name', + displayAsText: 'Name', + }, + { + id: 'package.version', + displayAsText: 'Version', + }, + { + id: 'package.architecture', + displayAsText: 'Architecture', + }, + { + id: 'vulnerability.severity', + displayAsText: 'Severity', + }, + { + id: 'vulnerability.id', + displayAsText: 'Id', + }, + { + id: 'vulnerability.score.version', + displayAsText: 'Score version', + }, + { + id: 'vulnerability.score.base', + displayAsText: 'Score', + }, + { + id: 'event.created', + displayAsText: 'Detected time', + } + ] \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.scss b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.scss new file mode 100644 index 0000000000..df8c71901f --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.scss @@ -0,0 +1,8 @@ +.vulsInventoryContainer { + height: calc(100vh - 104px); +} + + +.headerIsExpanded .vulsInventoryContainer { + height: calc(100vh - 153px); +} \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx index b94a76b6c7..717beef298 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx @@ -1,7 +1,168 @@ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; +import { getPlugins } from '../../../../../kibana-services'; +import useSearchBarConfiguration from '../../searchbar/use-search-bar-configuration' +import { IntlProvider } from 'react-intl'; +import { + EuiDataGrid, + EuiPageTemplate, + EuiToolTip, + EuiButtonIcon, + EuiDataGridCellValueElementProps, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, +} from '@elastic/eui'; +import { Filter, IndexPattern, OpenSearchQuerySortValue } from '../../../../../../../../src/plugins/data/common'; +import { SearchResponse } from '../../../../../../../../src/core/server'; +import DocViewer from '../../doc_viewer/doc_viewer'; +import { DiscoverNoResults } from '../../common/components/no_results'; +import { LoadingSpinner } from '../../common/components/loading_spinner'; +import { useDataGrid } from '../../data_grid/use_data_grid'; +import { inventoryTableDefaultColumns } from './config'; +import { useDocViewer } from '../../doc_viewer/use_doc_viewer'; +import './inventory.scss'; +import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; export const InventoryVuls = () => { + const { searchBarProps } = useSearchBarConfiguration({ + defaultIndexPatternID: VULNERABILITIES_INDEX_PATTERN_ID, + }) + const { isLoading, filters, query, indexPatterns } = searchBarProps; + const SearchBar = getPlugins().data.ui.SearchBar; + const [results, setResults] = useState({} as SearchResponse); + const [inspectedHit, setInspectedHit] = useState(undefined); + const [indexPattern, setIndexPattern] = useState(undefined); + const [isSearching, setIsSearching] = useState(false); + + + const onClickInspectDoc = useMemo(() => (index: number) => { + const rowClicked = results.hits.hits[index]; + setInspectedHit(rowClicked); + }, [results]); + + const DocViewInspectButton = ({ rowIndex }: EuiDataGridCellValueElementProps) => { + const inspectHintMsg = 'Inspect document details'; + + return ( + + onClickInspectDoc(rowIndex)} + iconType='inspect' + aria-label={inspectHintMsg} + /> + + ); + }; + + const dataGridProps = useDataGrid({ + ariaLabelledBy: 'Vulnerabilities Inventory Table', + defaultColumns: inventoryTableDefaultColumns, + results, + indexPattern: indexPattern as IndexPattern, + DocViewInspectButton + }) + + const { pagination, sorting } = dataGridProps; + + const docViewerProps = useDocViewer({ + doc: inspectedHit, + indexPattern: indexPattern as IndexPattern, + }) + + useEffect(() => { + if (!isLoading) { + setIndexPattern(indexPatterns?.[0] as IndexPattern); + try { + search(); + }catch(error){ + console.error(error); + // check when filters are wrong and the search fails + } + } + }, [JSON.stringify(searchBarProps), JSON.stringify(pagination), JSON.stringify(sorting)]); + + /** + * Search in index pattern + */ + const search = async (): Promise => { + const indexPattern = indexPatterns?.[0]; + if (indexPattern) { + setIsSearching(true); + const data = getPlugins().data + const searchSource = await data.search.searchSource.create(); + const timeFilter: Filter['query'] = [{ + range: { + '@timestamp': { + gte: searchBarProps?.dateRangeFrom, + lte: searchBarProps?.dateRangeTo, + format: 'strict_date_optional_time', + }, + }, + }] + const combined = [...timeFilter, ...(filters || [])]; + const fromField = (pagination?.pageIndex || 0) * (pagination?.pageSize || 100); + const sortOrder: OpenSearchQuerySortValue[] = sorting?.columns.map((column) => { + const sortDirection = column.direction === 'asc' ? 'asc' : 'desc'; + return { [column?.id || '']: sortDirection } as OpenSearchQuerySortValue; + }) || []; + + const results = await searchSource + .setParent(undefined) + .setField('filter', combined) + .setField('query', query) + .setField('sort', sortOrder) + .setField('size', pagination?.pageSize) + .setField('from', fromField) + .setField('index', indexPattern as IndexPattern) + .fetch(); + setResults(results); + setIsSearching(false); + } + }; + + const timeField = indexPattern?.timeFieldName ? indexPattern.timeFieldName : undefined; + + return ( -
Inventory dashboard
+ + + <> + {isLoading ? + : + } + {isSearching ? + : null} + {!isLoading && !isSearching && results?.hits?.total === 0 ? + : null} + {!isLoading && !isSearching && results?.hits?.total > 0 ? + : null} + {inspectedHit && ( + setInspectedHit(undefined)} size="m"> + + +

Document Details

+
+
+ + + + + + + +
+ )} + +
+
); } diff --git a/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts b/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts new file mode 100644 index 0000000000..7ef7cc50e0 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts @@ -0,0 +1,137 @@ +import { EuiDataGridCellValueElementProps, EuiDataGridColumn, EuiDataGridProps, EuiDataGridSorting } from "@elastic/eui" +import { useEffect, useMemo, useState } from "react"; +import { SearchResponse } from "@opensearch-project/opensearch/api/types"; +import { IFieldType, IndexPattern } from "../../../../../../../src/plugins/data/common"; + +type tDataGridProps = { + indexPattern: IndexPattern; + results: SearchResponse; + defaultColumns: EuiDataGridColumn[]; + DocViewInspectButton: ({ rowIndex }: EuiDataGridCellValueElementProps) => React.JSX.Element + ariaLabelledBy: string; +}; + +export const parseColumns = (fields: IFieldType[]): EuiDataGridColumn[] => { + return fields.map((field) => { + return { + id: field.name, + display: field.name, + schema: field.type, + actions: { + showHide: true, + }, + }; + }) || []; +} + +export const useDataGrid = (props: tDataGridProps): EuiDataGridProps => { + const { indexPattern, DocViewInspectButton, results, defaultColumns } = props; + /** Columns **/ + const [columns, setColumns] = useState(defaultColumns); + const [columnVisibility, setVisibility] = useState(() => + columns.map(({ id }) => id) + ); + /** Rows */ + const [rows, setRows] = useState([]); + const rowCount = results ? results?.hits?.total as number : 0; + /** Sorting **/ + // get default sorting from default columns + const getDefaultSorting = () => { + const defaultSort = columns.find((column) => column.isSortable || column.defaultSortDirection); + return defaultSort ? [{ id: defaultSort.id, direction: defaultSort.defaultSortDirection || 'desc' }] : []; + } + const defaultSorting: EuiDataGridSorting['columns'] = getDefaultSorting(); + const [sortingColumns, setSortingColumns] = useState(defaultSorting); + const onSort = (sortingColumns) => {setSortingColumns(sortingColumns)}; + /** Pagination **/ + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 }); + const onChangeItemsPerPage = useMemo(() => (pageSize) => + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), [rows, rowCount]); + const onChangePage = (pageIndex) => setPagination((pagination) => ({ ...pagination, pageIndex })) + + useEffect(() => { + setRows(results?.hits?.hits || []) + }, [results, results?.hits, results?.hits?.total]) + + + useEffect(() => { + setPagination((pagination) => ({ ...pagination, pageIndex: 0 })); + }, [rowCount]) + + const parseData = (resultsHits: SearchResponse['hits']['hits']): any[] => { + const data = resultsHits.map((hit) => { + if (!hit) { + return {} + } + const source = hit._source as object; + const data = { + ...source, + _id: hit._id, + _index: hit._index, + _type: hit._type, + _score: hit._score, + }; + return data; + }); + return data; + } + + const renderCellValue = ({ rowIndex, columnId, setCellProps }) => { + const rowsParsed = parseData(rows); + function getFormatted(rowIndex, columnId) { + if (columnId.includes('.')) { + // when the column is a nested field. The column could have 2 to n levels + // get dinamically the value of the nested field + const nestedFields = columnId.split('.'); + let value = rowsParsed[rowIndex]; + nestedFields.forEach((field) => { + if (value) { + value = value[field]; + } + }); + return value; + } else { + return rowsParsed[rowIndex][columnId].formatted + ? rowsParsed[rowIndex][columnId].formatted + : rowsParsed[rowIndex][columnId]; + } + } + // On the context data always is stored the current page data (pagination) + // then the rowIndex is relative to the current page + const relativeRowIndex = rowIndex % pagination.pageSize; + return rowsParsed.hasOwnProperty(relativeRowIndex) + ? getFormatted(relativeRowIndex, columnId) + : null; + }; + + const leadingControlColumns = useMemo(() => { + return [ + { + id: 'inspectCollapseColumn', + headerCellRender: () => null, + rowCellRender: (props) => DocViewInspectButton({ ...props, rowIndex: props.rowIndex % pagination.pageSize }), + width: 40, + }, + ]; + }, [results]); + + return { + "aria-labelledby": props.ariaLabelledBy, + columns: parseColumns(indexPattern?.fields || []), + columnVisibility: { visibleColumns: columnVisibility, setVisibleColumns: setVisibility }, + renderCellValue: renderCellValue, + leadingControlColumns: leadingControlColumns, + rowCount, + sorting: { columns: sortingColumns, onSort }, + pagination: { + ...pagination, + pageSizeOptions: [20, 50, 100], + onChangeItemsPerPage: onChangeItemsPerPage, + onChangePage: onChangePage, + } + } +} \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/doc_viewer/doc_viewer.tsx b/plugins/main/public/components/overview/vulnerabilities/doc_viewer/doc_viewer.tsx new file mode 100644 index 0000000000..08e170f9d5 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/doc_viewer/doc_viewer.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react'; +import classNames from 'classnames'; +import { escapeRegExp } from 'lodash'; +import { i18n } from '@osd/i18n'; +import { FieldIcon } from '../../../../../../../src/plugins/opensearch_dashboards_react/public'; +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; + +const COLLAPSE_LINE_LENGTH = 350; +const DOT_PREFIX_RE = /(.).+?\./g; + +export type tDocViewerProps = { + flattened: any; + formatted: any; + mapping: any; + indexPattern: any; +} + +/** + * Convert a dot.notated.string into a short + * version (d.n.string) + */ +export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.'); + +export function getFieldTypeName(type: string) { + switch (type) { + case 'boolean': + return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { + defaultMessage: 'Boolean field', + }); + case 'conflict': + return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { + defaultMessage: 'Conflicting field', + }); + case 'date': + return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { + defaultMessage: 'Date field', + }); + case 'geo_point': + return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { + defaultMessage: 'Geo point field', + }); + case 'geo_shape': + return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { + defaultMessage: 'Geo shape field', + }); + case 'ip': + return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { + defaultMessage: 'IP address field', + }); + case 'murmur3': + return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { + defaultMessage: 'Murmur3 field', + }); + case 'number': + return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { + defaultMessage: 'Number field', + }); + case 'source': + // Note that this type is currently not provided, type for _source is undefined + return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { + defaultMessage: 'Source field', + }); + case 'string': + return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { + defaultMessage: 'String field', + }); + case 'nested': + return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { + defaultMessage: 'Nested field', + }); + default: + return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', { + defaultMessage: 'Unknown field', + }); + } +} + +const DocViewer = (props: tDocViewerProps) => { + const [fieldRowOpen, setFieldRowOpen] = useState({} as Record); + const { flattened, formatted, mapping, indexPattern } = props; + + return (<> + {flattened && ( + + + {Object.keys(flattened) + .sort() + .map((field, index) => { + const value = String(formatted[field]); + const fieldMapping = mapping(field); + const isCollapsible = value.length > COLLAPSE_LINE_LENGTH; + const isCollapsed = isCollapsible && !fieldRowOpen[field]; + const valueClassName = classNames({ + // eslint-disable-next-line @typescript-eslint/naming-convention + osdDocViewer__value: true, + 'truncate-by-height': isCollapsible && isCollapsed, + }); + const isNestedField = + !indexPattern.fields.getByName(field) && + !!indexPattern.fields.getAll().find((patternField) => { + // We only want to match a full path segment + const nestedRootRegex = new RegExp(escapeRegExp(field) + '(\\.|$)'); + return nestedRootRegex.test(patternField.subType?.nested?.path ?? ''); + }); + const fieldType = isNestedField ? 'nested' : indexPattern.fields.getByName(field)?.type; + const typeName = getFieldTypeName(String(fieldType)); + const displayName = field; + const fieldIconProps = { fill: 'none', color: 'gray' } + const scripted = Boolean(fieldMapping?.scripted) + + return ( + + + + + ); + })} + +
+ + + + + + + {displayName} + + + + +
+
+ )}) +}; + +export default DocViewer; \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/doc_viewer/use_doc_viewer.ts b/plugins/main/public/components/overview/vulnerabilities/doc_viewer/use_doc_viewer.ts new file mode 100644 index 0000000000..d38e58bc3a --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/doc_viewer/use_doc_viewer.ts @@ -0,0 +1,28 @@ +import { tDocViewerProps } from "./doc_viewer" +import { IndexPattern } from "../../../../../../../src/plugins/data/common"; + +type tUseDocViewerInputs = { + indexPattern: IndexPattern; + doc: any; +} + +export const useDocViewer = (props: tUseDocViewerInputs): tDocViewerProps => { + const { indexPattern, doc } = props; + + if (!indexPattern || !doc) { + return { + flattened: {}, + formatted: {}, + indexPattern: undefined, + mapping: undefined + } + } + + const mapping = indexPattern?.fields.getByName; + return { + flattened: indexPattern?.flattenHit(doc), + formatted: indexPattern?.formatHit(doc, 'html'), + indexPattern, + mapping + } +} \ No newline at end of file From d78defcddc37b3bcec5d8f2cb25ac183bc6a4d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Biset?= <43619595+jbiset@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:14:07 -0300 Subject: [PATCH 09/20] Feat/5894 vulnerabilities dashboard create the dashboard tab using osd plugins (#5966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add useDashboardConfiguration and unit test hook * Changed how the initial hook configuration is set * Create dashboard using embedded visualizations by value. * [Vulnerabilities dashboards] Add search bar services (#5960) * Add search bar hooks * Rename searchbar hook * Add unit test to use search bar configuration hook * Fix some unit test titles * Remove console.log * Solve requested changes * Fix request changes and hook filename * [Vulnerabilities dashboards] Fix wrong agent.id filters loaded in search bar by default (#5970) Remove agent id filter in searchbar * Recommended filters are added and communication problems between the dashboard and the searchbar are solved * Update Vulnerability detector dashboard filters visualization and VULNERABILITIES_INDEX_PATTERN_ID constant * Change KPI dashboard and fix bad request * Separates filter panels from dashboard panels * Add Accumulation of the most detected vulnerabilities visualization and change --------- Co-authored-by: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> --- .../dashboard-filters/dashboard-filters.tsx | 37 + .../dashboard-panels-filters.ts | 160 +++ .../vulnerability-detector-filters.scss | 6 + .../dashboards/overview/dashboard-panels.ts | 1227 +++++++++++++++++ .../dashboards/overview/dashboard.tsx | 101 +- 5 files changed, 1528 insertions(+), 3 deletions(-) create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-filters.tsx create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-panels-filters.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/vulnerability-detector-filters.scss create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-filters.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-filters.tsx new file mode 100644 index 0000000000..010a370972 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-filters.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { getPlugins } from '../../../../../../kibana-services'; +import { ViewMode } from '../../../../../../../../../src/plugins/embeddable/public'; +import './vulnerability-detector-filters.scss'; +import { getDashboardFilters } from './dashboard-panels-filters'; + +const plugins = getPlugins(); +const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer; + +export const DashboardFilters = (searchBarProps: any) => { + return ( +
+ +
+ ); +}; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-panels-filters.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-panels-filters.ts new file mode 100644 index 0000000000..7c48f1e65d --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-panels-filters.ts @@ -0,0 +1,160 @@ +import { DashboardPanelState } from "../../../../../../../../../src/plugins/dashboard/public/application"; +import { EmbeddableInput } from "../../../../../../../../../src/plugins/embeddable/public"; +import { VULNERABILITIES_INDEX_PATTERN_ID } from "../../../common/constants"; + +const getVisStateFilter = ( + id: string, + indexPatternId: string, + title: string, + label: string, + fieldName: string, + ) => { + return { + id, + title, + type: 'table', + params: { + perPage: 5, + percentageCol: '', + row: true, + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + totalFunc: 'sum', + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: 'Count', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: fieldName, + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: label, + }, + schema: 'bucket', + }, + ], + }, + }; + }; + + + export const getDashboardFilters = (): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; + } => { + return { + topPackageSelector: { + gridData: { + w: 12, + h: 12, + x: 0, + y: 0, + i: 'topPackageSelector', + }, + type: 'visualization', + explicitInput: { + id: 'topPackageSelector', + savedVis: getVisStateFilter( + 'topPackageSelector', + VULNERABILITIES_INDEX_PATTERN_ID, + 'Top Packages vulnerabilities', + 'Package', + 'package.name', + ), + }, + }, + topOSVulnerabilities: { + gridData: { + w: 12, + h: 12, + x: 12, + y: 0, + i: 'topOSVulnerabilities', + }, + type: 'visualization', + explicitInput: { + id: 'topOSVulnerabilities', + savedVis: getVisStateFilter( + 'topOSVulnerabilities', + VULNERABILITIES_INDEX_PATTERN_ID, + 'Top Operating system vulnerabilities', + 'Operating system', + 'host.os.name', + ), + }, + }, + topAgentVulnerabilities: { + gridData: { + w: 12, + h: 12, + x: 24, + y: 0, + i: 'topAgentVulnerabilities', + }, + type: 'visualization', + explicitInput: { + id: 'topAgentVulnerabilities', + savedVis: getVisStateFilter( + 'topAgentVulnerabilities', + VULNERABILITIES_INDEX_PATTERN_ID, + 'Agent filter', + 'Agent', + 'agent.id', + ), + }, + }, + topVulnerabilities: { + gridData: { + w: 12, + h: 12, + x: 36, + y: 0, + i: 'topVulnerabilities', + }, + type: 'visualization', + explicitInput: { + id: 'topVulnerabilities', + savedVis: getVisStateFilter( + 'topVulnerabilities', + VULNERABILITIES_INDEX_PATTERN_ID, + 'Top vulnerabilities', + 'Vulnerability', + 'vulnerability.id', + ), + }, + }, + }; + }; \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/vulnerability-detector-filters.scss b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/vulnerability-detector-filters.scss new file mode 100644 index 0000000000..631158a73e --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/vulnerability-detector-filters.scss @@ -0,0 +1,6 @@ +.vulnerability-dashboard-filters-wrapper { + .euiDataGrid__controls,.euiDataGrid__pagination { + display: none!important; + } +} + diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts new file mode 100644 index 0000000000..424dc48278 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts @@ -0,0 +1,1227 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; +import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; + +const getVisStateSeverityCritical = (indexPatternId: string) => { + return { + id: 'severity_critical_vulnerabilities', + title: 'Critical', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Reds', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 50, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: ' ', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'vulnerability.severity:"critical"', + language: 'kuery', + }, + label: '- Critical Severity Alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateSeverityHigh = (indexPatternId: string) => { + return { + id: 'severity_high_vulnerabilities', + title: 'High', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Blues', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 50, + }, + }, + }, + uiState: { + vis: { + colors: { + 'High Severity Alerts - Count': '#38D1BA', + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: ' ', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'vulnerability.severity:"high"', + language: 'kuery', + }, + label: '- High Severity Alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateSeverityMedium = (indexPatternId: string) => { + return { + id: 'severity_medium_vulnerabilities', + title: 'Medium', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Yellow to Red', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: true, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 50, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: ' ', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'vulnerability.severity:"medium"', + language: 'kuery', + }, + label: '- Medium Severity Alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateSeverityLow = (indexPatternId: string) => { + return { + id: 'severity_low_vulnerabilities', + title: 'Low', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 50, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: ' ', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'vulnerability.severity:"low"', + language: 'kuery', + }, + label: '- Low Severity Alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateTopVulnerabilities = (indexPatternId: string) => { + return { + id: 'most_detected_vulnerabilities', + title: 'Most detected vulnerabilities', + type: 'horizontal_bar', + params: { + type: 'histogram', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 200, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 75, + filter: true, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'vulnerability.id', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Vulnerability.ID', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'vulnerability.id', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateTopVulnerabilitiesEndpoints = (indexPatternId: string) => { + return { + id: 'most_vulnerable_endpoints_vulnerabilities', + title: 'The most vulnerable endpoints', + type: 'horizontal_bar', + params: { + type: 'histogram', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 200, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 75, + filter: true, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, + uiState: { + vis: { + legendOpen: false, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: 'Count', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'agent.id', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'package.path', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'agent.id', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'mm', + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateAccumulationMostDetectedVulnerabilities = ( + indexPatternId: string, +) => { + return { + id: 'accumulation_most_vulnerable_vulnerabilities', + title: 'Accumulation of the most detected vulnerabilities', + type: 'heatmap', + params: { + addLegend: true, + addTooltip: true, + colorSchema: 'Greens', + colorsNumber: 5, + colorsRange: [ + { + from: 0, + to: 100, + }, + ], + enableHover: false, + invertColors: false, + legendPosition: 'right', + percentageMode: false, + setColorRange: false, + times: [], + type: 'heatmap', + valueAxes: [ + { + id: 'ValueAxis-1', + labels: { + color: 'black', + overwriteColor: false, + rotate: 0, + show: false, + }, + scale: { + defaultYExtents: false, + type: 'linear', + }, + show: false, + type: 'value', + }, + ], + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'vulnerability.id', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + { + id: '3', + enabled: true, + type: 'date_histogram', + params: { + field: '@timestamp', + timeRange: { + from: 'now-90d', + to: 'now', + }, + useNormalizedOpenSearchInterval: true, + scaleMetricValues: false, + interval: 'w', + // eslint-disable-next-line camelcase + drop_partials: false, + // eslint-disable-next-line camelcase + min_doc_count: 1, + // eslint-disable-next-line camelcase + extended_bounds: {}, + }, + schema: 'segment', + }, + ], + }, + }; +}; + +const getVisStateInventoryTable = (indexPatternId: string) => { + return { + id: 'inventory_table_vulnerabilities', + title: 'Inventory table', + type: 'table', + params: { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: 'Count', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'package.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'name', + }, + schema: 'bucket', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'package.version', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'version', + }, + schema: 'bucket', + }, + { + id: '4', + enabled: true, + type: 'terms', + params: { + field: 'package.architecture', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'architecture', + }, + schema: 'bucket', + }, + { + id: '5', + enabled: true, + type: 'terms', + params: { + field: 'vulnerability.severity', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'severity', + }, + schema: 'bucket', + }, + { + id: '6', + enabled: true, + type: 'terms', + params: { + field: 'vulnerability.id', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'id', + }, + schema: 'bucket', + }, + { + id: '7', + enabled: true, + type: 'terms', + params: { + field: 'vulnerability.score.version', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'score version', + }, + schema: 'bucket', + }, + { + id: '8', + enabled: true, + type: 'terms', + params: { + field: 'vulnerability.score.base', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'score base', + }, + schema: 'bucket', + }, + ], + }, + }; +}; + +const getVisStateOpenVsCloseVulnerabilities = (indexPatternId: string) => { + return { + id: 'open_vs_close_vulnerabilities', + title: 'Open vs. Close', + type: 'line', + params: { + type: 'line', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'line', + mode: 'normal', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + interpolate: 'linear', + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: 'Count', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-1d', + to: 'now', + }, + useNormalizedOpenSearchInterval: true, + scaleMetricValues: false, + interval: 'auto', + // eslint-disable-next-line camelcase + drop_partials: false, + // eslint-disable-next-line camelcase + min_doc_count: 1, + // eslint-disable-next-line camelcase + extended_bounds: {}, + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.vulnerability.state', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + exclude: 'Pending confirmation', + }, + schema: 'group', + }, + ], + }, + }; +}; + +export const getKPIsPanel = (): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + '1': { + gridData: { + w: 12, + h: 6, + x: 0, + y: 0, + i: '1', + }, + type: 'visualization', + explicitInput: { + id: '1', + savedVis: getVisStateSeverityCritical(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + '2': { + gridData: { + w: 12, + h: 6, + x: 12, + y: 0, + i: '2', + }, + type: 'visualization', + explicitInput: { + id: '2', + savedVis: getVisStateSeverityHigh(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + '3': { + gridData: { + w: 12, + h: 6, + x: 24, + y: 0, + i: '3', + }, + type: 'visualization', + explicitInput: { + id: '3', + savedVis: getVisStateSeverityMedium(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + '4': { + gridData: { + w: 12, + h: 6, + x: 36, + y: 0, + i: '4', + }, + type: 'visualization', + explicitInput: { + id: '4', + savedVis: getVisStateSeverityLow(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + }; +}; + +export const getOpenVsClosePanel = (): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + '5': { + gridData: { + w: 48, + h: 12, + x: 0, + y: 0, + i: '5', + }, + type: 'visualization', + explicitInput: { + id: '5', + savedVis: getVisStateOpenVsCloseVulnerabilities('wazuh-alerts-*'), + }, + }, + }; +}; + +export const getDashboardPanels = (): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + '6': { + gridData: { + w: 16, + h: 12, + x: 0, + y: 0, + i: '6', + }, + type: 'visualization', + explicitInput: { + id: '6', + savedVis: getVisStateTopVulnerabilities( + VULNERABILITIES_INDEX_PATTERN_ID, + ), + }, + }, + '7': { + gridData: { + w: 16, + h: 12, + x: 16, + y: 0, + i: '7', + }, + type: 'visualization', + explicitInput: { + id: '7', + savedVis: getVisStateTopVulnerabilitiesEndpoints( + VULNERABILITIES_INDEX_PATTERN_ID, + ), + }, + }, + '8': { + gridData: { + w: 16, + h: 12, + x: 32, + y: 0, + i: '8', + }, + type: 'visualization', + explicitInput: { + id: '8', + savedVis: getVisStateAccumulationMostDetectedVulnerabilities( + VULNERABILITIES_INDEX_PATTERN_ID, + ), + }, + }, + '9': { + gridData: { + w: 48, + h: 12, + x: 0, + y: 14, + i: '9', + }, + type: 'visualization', + explicitInput: { + id: '9', + savedVis: getVisStateInventoryTable(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + }; +}; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx index db923f1eb0..b11dad7484 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx @@ -1,7 +1,102 @@ import React from 'react'; +import { getPlugins } from '../../../../../kibana-services'; +import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; +import { getDashboardPanels, getKPIsPanel, getOpenVsClosePanel } from './dashboard-panels'; +import { I18nProvider } from '@osd/i18n/react'; +import useSearchBarConfiguration from '../../searchbar/use-search-bar-configuration'; +import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; +import { DashboardFilters } from './dashboard-filters/dashboard-filters'; + +const plugins = getPlugins(); + +const SearchBar = getPlugins().data.ui.SearchBar; + +const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer; + +/* The vulnerabilities dashboard is made up of 3 dashboards because the filters need +a wrapper for visual adjustments, while the Kpi, the Open vs Close visualization and +the rest of the visualizations have different configurations at the dashboard level. */ + +export const DashboardVuls: React.FC = () => { + const { searchBarProps } = useSearchBarConfiguration({ + defaultIndexPatternID: VULNERABILITIES_INDEX_PATTERN_ID, + filters: [], + }); -export const DashboardVuls = () => { return ( -
Vulnerabilities Dashboard
+ <> + + + + + + + + ); -} +}; From fcf1e782b053dcc0a797e05b07a6d84b9d170583 Mon Sep 17 00:00:00 2001 From: jbiset Date: Wed, 25 Oct 2023 11:54:01 -0300 Subject: [PATCH 10/20] Fix default filters on usesearchbar configuration --- .../common/hooks/use-filter-manager.ts | 31 ++- .../dashboards/overview/dashboard.tsx | 14 +- .../use-search-bar-configuration.tsx | 240 ++++++++++-------- 3 files changed, 161 insertions(+), 124 deletions(-) diff --git a/plugins/main/public/components/common/hooks/use-filter-manager.ts b/plugins/main/public/components/common/hooks/use-filter-manager.ts index 884279668c..5d02f42b16 100644 --- a/plugins/main/public/components/common/hooks/use-filter-manager.ts +++ b/plugins/main/public/components/common/hooks/use-filter-manager.ts @@ -11,29 +11,36 @@ */ import { getDataPlugin } from '../../../kibana-services'; import { useState, useEffect, useMemo } from 'react'; -import { Filter } from 'src/plugins/data/public'; +import { Filter } from '../../../../../../src/plugins/data/public'; import _ from 'lodash'; import { FilterManager } from '../../../../../../src/plugins/data/public'; +import { Subscription } from 'rxjs'; type tUseFilterManagerReturn = { filterManager: FilterManager; filters: Filter[]; -} +}; -export const useFilterManager = () : tUseFilterManagerReturn => { - const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); +export const useFilterManager = (): tUseFilterManagerReturn => { + const filterManager = getDataPlugin().query.filterManager; const [filters, setFilters] = useState(filterManager.getFilters()); useEffect(() => { - const subscription = filterManager.getUpdates$().subscribe(() => { - const newFilters = filterManager.getFilters(); - if (!_.isEqual(filters, newFilters)) { - setFilters(newFilters); - } - }); + const subscriptions = new Subscription(); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + const newFilters = filterManager.getFilters(); + setFilters(newFilters); + }, + }) + ); + return () => { - subscription.unsubscribe(); + subscriptions.unsubscribe(); }; - }, []); + }, [filterManager]); + return { filterManager, filters }; }; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx index b11dad7484..f2cd69425e 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx @@ -1,7 +1,11 @@ import React from 'react'; import { getPlugins } from '../../../../../kibana-services'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; -import { getDashboardPanels, getKPIsPanel, getOpenVsClosePanel } from './dashboard-panels'; +import { + getDashboardPanels, + getKPIsPanel, + getOpenVsClosePanel, +} from './dashboard-panels'; import { I18nProvider } from '@osd/i18n/react'; import useSearchBarConfiguration from '../../searchbar/use-search-bar-configuration'; import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; @@ -26,7 +30,10 @@ export const DashboardVuls: React.FC = () => { return ( <> - + { to: searchBarProps.dateRangeTo, }, title: 'Open vs Close Vulnerabilities', - description: 'Open vs Close Vulnerabilities of the Vulnerability detector', + description: + 'Open vs Close Vulnerabilities of the Vulnerability detector', query: searchBarProps.query, refreshConfig: { pause: false, diff --git a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx b/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx index ee8b629263..335f17be53 100644 --- a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx @@ -1,137 +1,159 @@ -import React, { useEffect, useState } from 'react' -import { SearchBarProps, FilterManager, TimeRange, Query } from '../../../../../../../src/plugins/data/public' -import { Filter, IIndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; +import React, { useEffect, useState } from 'react'; +import { + SearchBarProps, + FilterManager, + TimeRange, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { + Filter, + IIndexPattern, + IndexPatternsContract, +} from '../../../../../../../src/plugins/data/public'; import { getDataPlugin } from '../../../../kibana-services'; import { useFilterManager, useQueryManager, useTimeFilter } from '../../../common/hooks'; import { AUTHORIZED_AGENTS } from '../../../../../common/constants'; +import { VULNERABILITIES_INDEX_PATTERN_ID } from '../common/constants'; // Input - types type tUseSearchBarCustomInputs = { - defaultIndexPatternID?: IIndexPattern['id']; - onFiltersUpdated?: (filters: Filter[]) => void; - onQuerySubmitted?: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; -} + defaultIndexPatternID?: IIndexPattern['id']; + onFiltersUpdated?: (filters: Filter[]) => void; + onQuerySubmitted?: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; +}; type tUseSearchBarProps = Partial & tUseSearchBarCustomInputs; // Output types type tUserSearchBarResponse = { - searchBarProps: Partial; -} + searchBarProps: Partial; +}; /** * Hook used to compose the searchbar configuration props - * @param props - * @returns + * @param props + * @returns */ const useSearchBarConfiguration = (props?: tUseSearchBarProps): tUserSearchBarResponse => { - // dependencies - const filterManager = useFilterManager().filterManager as FilterManager; - const [query, setQuery] = props?.query ? useState(props?.query) : useQueryManager(); - const { timeFilter, timeHistory, setTimeFilter } = useTimeFilter(); - // states - const [isLoading, setIsLoading] = useState(false); - const [indexPatternSelected, setIndexPatternSelected] = useState(); + // dependencies + const filterManager = useFilterManager().filterManager as FilterManager; + const { filters } = useFilterManager(); + const [query, setQuery] = props?.query ? useState(props?.query) : useQueryManager(); + const { timeFilter, timeHistory, setTimeFilter } = useTimeFilter(); + // states + const [isLoading, setIsLoading] = useState(false); + const [indexPatternSelected, setIndexPatternSelected] = useState(); - useEffect(() => { - setIsLoading(true); - initSearchBar(); - setIsLoading(false); - }, []); + useEffect(() => { + initSearchBar(); + }, []); - /** - * Initialize the searchbar props with the corresponding index pattern and filters - */ - const initSearchBar = async () => { - const indexPattern = await getIndexPattern(props?.defaultIndexPatternID); - setIndexPatternSelected(indexPattern); - const filters = await getInitialFilters(indexPattern); - filterManager.setFilters(filters); + useEffect(() => { + const defaultIndex = props?.defaultIndexPatternID ?? VULNERABILITIES_INDEX_PATTERN_ID; + /* Filters that do not belong to the default index are filtered */ + const cleanedFilters = filters.filter((filter) => filter.meta.index === defaultIndex); + if (cleanedFilters.length !== filters.length) { + filterManager.setFilters(cleanedFilters); } + }, [filters]); - /** - * Return the index pattern data by ID. - * If not receive a ID return the default index from the index pattern service - * @returns - */ - const getIndexPattern = async (indexPatternID?: string) => { - const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; - if (indexPatternID) { - try { - return await indexPatternService.get(indexPatternID); - } catch (error) { - // when the index pattern id not exists will get the default - console.error(error); - return await indexPatternService.getDefault(); - } - } else { - return await indexPatternService.getDefault(); - } + /** + * Initialize the searchbar props with the corresponding index pattern and filters + */ + const initSearchBar = async () => { + setIsLoading(true); + const indexPattern = await getIndexPattern(props?.defaultIndexPatternID); + setIndexPatternSelected(indexPattern); + const filters = await getInitialFilters(indexPattern); + filterManager.setFilters(filters); + setIsLoading(false); + }; + /** + * Return the index pattern data by ID. + * If not receive a ID return the default index from the index pattern service + * @returns + */ + const getIndexPattern = async (indexPatternID?: string) => { + const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; + if (indexPatternID) { + try { + return await indexPatternService.get(indexPatternID); + } catch (error) { + // when the index pattern id not exists will get the default + console.error(error); + return await indexPatternService.getDefault(); + } + } else { + return await indexPatternService.getDefault(); } + }; - /** - * Return the initial filters considering if hook receives initial filters - * When the default index pattern is the same like the received preserve the filters - * @param indexPattern - * @returns - */ - const getInitialFilters = async (indexPattern: IIndexPattern) => { - const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; - let initialFilters: Filter[] = []; - if (props?.filters) { - return props?.filters; - } - if (indexPattern) { - // get filtermanager and filters - // if the index is the same, get filters stored - // else clear filters - const defaultIndexPattern = await indexPatternService.getDefault() as IIndexPattern; - initialFilters = defaultIndexPattern.id === indexPattern.id ? filterManager.getFilters() : [] - } else { - initialFilters = []; - } - return initialFilters; + /** + * Return the initial filters considering if hook receives initial filters + * When the default index pattern is the same like the received preserve the filters + * @param indexPattern + * @returns + */ + const getInitialFilters = async (indexPattern: IIndexPattern) => { + const indexPatternService = getDataPlugin().indexPatterns as IndexPatternsContract; + let initialFilters: Filter[] = []; + if (props?.filters) { + return props?.filters; } - - /** - * Return filters from filters manager. - * Additionally solve the known issue with the auto loaded agent.id filters from the searchbar - * @returns - */ - const getFilters = () => { - const filters = filterManager ? filterManager.getFilters() : []; - return filters.filter(filter => filter.meta.controlledBy !== AUTHORIZED_AGENTS); // remove auto loaded agent.id filters + if (indexPattern) { + // get filtermanager and filters + // if the index is the same, get filters stored + // else clear filters + const defaultIndexPattern = (await indexPatternService.getDefault()) as IIndexPattern; + initialFilters = defaultIndexPattern.id === indexPattern.id ? filterManager.getFilters() : []; + } else { + initialFilters = []; } + return initialFilters; + }; - /** - * Search bar properties necessary to render and initialize the osd search bar component - */ - const searchBarProps: Partial = { - isLoading, - ...indexPatternSelected && { indexPatterns: [indexPatternSelected] }, // indexPattern cannot be empty or empty [] - filters: getFilters(), - query, - timeHistory, - dateRangeFrom: timeFilter.from, - dateRangeTo: timeFilter.to, - onFiltersUpdated: (filters: Filter[]) => { - // its necessary execute setter to apply filters - filterManager.setFilters(filters); - props?.onFiltersUpdated && props?.onFiltersUpdated(filters); - }, - onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }, _isUpdate?: boolean): void => { - const { dateRange, query } = payload; - // its necessary execute setter to apply query filters - setTimeFilter(dateRange); - setQuery(query); - props?.onQuerySubmitted && props?.onQuerySubmitted(payload); - } - } + /** + * Return filters from filters manager. + * Additionally solve the known issue with the auto loaded agent.id filters from the searchbar + * @returns + */ + const getFilters = () => { + const filters = filterManager ? filterManager.getFilters() : []; + return filters.filter((filter) => filter.meta.controlledBy !== AUTHORIZED_AGENTS); // remove auto loaded agent.id filters + }; - return { - searchBarProps, - } -} + /** + * Search bar properties necessary to render and initialize the osd search bar component + */ + const searchBarProps: Partial = { + isLoading, + ...(indexPatternSelected && { indexPatterns: [indexPatternSelected] }), // indexPattern cannot be empty or empty [] + filters: getFilters(), + query, + timeHistory, + dateRangeFrom: timeFilter.from, + dateRangeTo: timeFilter.to, + onFiltersUpdated: (filters: Filter[]) => { + // its necessary execute setter to apply filters + filterManager.setFilters(filters); + props?.onFiltersUpdated && props?.onFiltersUpdated(filters); + }, + onQuerySubmit: ( + payload: { dateRange: TimeRange; query?: Query }, + _isUpdate?: boolean + ): void => { + const { dateRange, query } = payload; + // its necessary execute setter to apply query filters + setTimeFilter(dateRange); + setQuery(query); + props?.onQuerySubmitted && props?.onQuerySubmitted(payload); + }, + }; + + return { + searchBarProps, + }; +}; -export default useSearchBarConfiguration; \ No newline at end of file +export default useSearchBarConfiguration; From 73cd0716a3a6a085c5e836b0453eb43cff64689d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Biset?= <43619595+jbiset@users.noreply.github.com> Date: Fri, 27 Oct 2023 12:51:26 -0300 Subject: [PATCH 11/20] [Vulnerabilities dashboards] Fix vulnerability dashboard filters (#6065) Fix vulnerability dashboard filters --- .../dashboard-filters/dashboard-filters.tsx | 37 -- .../dashboard-panels-filters.ts | 160 ----- .../overview/dashboard-panel-open-vs-close.ts | 176 ++++++ .../overview/dashboard-panels-filters.ts | 159 +++++ .../overview/dashboard-panels-kpis.ts | 415 +++++++++++++ .../dashboards/overview/dashboard-panels.ts | 586 ------------------ .../dashboards/overview/dashboard.tsx | 39 +- .../vulnerability-detector-filters.scss | 0 8 files changed, 779 insertions(+), 793 deletions(-) delete mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-filters.tsx delete mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-panels-filters.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panel-open-vs-close.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-filters.ts create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-kpis.ts rename plugins/main/public/components/overview/vulnerabilities/dashboards/overview/{dashboard-filters => }/vulnerability-detector-filters.scss (100%) diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-filters.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-filters.tsx deleted file mode 100644 index 010a370972..0000000000 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-filters.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { getPlugins } from '../../../../../../kibana-services'; -import { ViewMode } from '../../../../../../../../../src/plugins/embeddable/public'; -import './vulnerability-detector-filters.scss'; -import { getDashboardFilters } from './dashboard-panels-filters'; - -const plugins = getPlugins(); -const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer; - -export const DashboardFilters = (searchBarProps: any) => { - return ( -
- -
- ); -}; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-panels-filters.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-panels-filters.ts deleted file mode 100644 index 7c48f1e65d..0000000000 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/dashboard-panels-filters.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { DashboardPanelState } from "../../../../../../../../../src/plugins/dashboard/public/application"; -import { EmbeddableInput } from "../../../../../../../../../src/plugins/embeddable/public"; -import { VULNERABILITIES_INDEX_PATTERN_ID } from "../../../common/constants"; - -const getVisStateFilter = ( - id: string, - indexPatternId: string, - title: string, - label: string, - fieldName: string, - ) => { - return { - id, - title, - type: 'table', - params: { - perPage: 5, - percentageCol: '', - row: true, - showMetricsAtAllLevels: false, - showPartialRows: false, - showTotal: false, - totalFunc: 'sum', - }, - data: { - searchSource: { - query: { - language: 'kuery', - query: '', - }, - index: indexPatternId, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: indexPatternId, - }, - ], - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: { - customLabel: 'Count', - }, - schema: 'metric', - }, - { - id: '2', - enabled: true, - type: 'terms', - params: { - field: fieldName, - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: label, - }, - schema: 'bucket', - }, - ], - }, - }; - }; - - - export const getDashboardFilters = (): { - [panelId: string]: DashboardPanelState< - EmbeddableInput & { [k: string]: unknown } - >; - } => { - return { - topPackageSelector: { - gridData: { - w: 12, - h: 12, - x: 0, - y: 0, - i: 'topPackageSelector', - }, - type: 'visualization', - explicitInput: { - id: 'topPackageSelector', - savedVis: getVisStateFilter( - 'topPackageSelector', - VULNERABILITIES_INDEX_PATTERN_ID, - 'Top Packages vulnerabilities', - 'Package', - 'package.name', - ), - }, - }, - topOSVulnerabilities: { - gridData: { - w: 12, - h: 12, - x: 12, - y: 0, - i: 'topOSVulnerabilities', - }, - type: 'visualization', - explicitInput: { - id: 'topOSVulnerabilities', - savedVis: getVisStateFilter( - 'topOSVulnerabilities', - VULNERABILITIES_INDEX_PATTERN_ID, - 'Top Operating system vulnerabilities', - 'Operating system', - 'host.os.name', - ), - }, - }, - topAgentVulnerabilities: { - gridData: { - w: 12, - h: 12, - x: 24, - y: 0, - i: 'topAgentVulnerabilities', - }, - type: 'visualization', - explicitInput: { - id: 'topAgentVulnerabilities', - savedVis: getVisStateFilter( - 'topAgentVulnerabilities', - VULNERABILITIES_INDEX_PATTERN_ID, - 'Agent filter', - 'Agent', - 'agent.id', - ), - }, - }, - topVulnerabilities: { - gridData: { - w: 12, - h: 12, - x: 36, - y: 0, - i: 'topVulnerabilities', - }, - type: 'visualization', - explicitInput: { - id: 'topVulnerabilities', - savedVis: getVisStateFilter( - 'topVulnerabilities', - VULNERABILITIES_INDEX_PATTERN_ID, - 'Top vulnerabilities', - 'Vulnerability', - 'vulnerability.id', - ), - }, - }, - }; - }; \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panel-open-vs-close.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panel-open-vs-close.ts new file mode 100644 index 0000000000..aaceb7af69 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panel-open-vs-close.ts @@ -0,0 +1,176 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; + +const getVisStateOpenVsCloseVulnerabilities = (indexPatternId: string) => { + return { + id: 'open_vs_close_vulnerabilities', + title: 'Open vs. Close', + type: 'line', + params: { + type: 'line', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'line', + mode: 'normal', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + interpolate: 'linear', + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: 'Count', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-1d', + to: 'now', + }, + useNormalizedOpenSearchInterval: true, + scaleMetricValues: false, + interval: 'auto', + // eslint-disable-next-line camelcase + drop_partials: false, + // eslint-disable-next-line camelcase + min_doc_count: 1, + // eslint-disable-next-line camelcase + extended_bounds: {}, + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.vulnerability.state', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + exclude: 'Pending confirmation', + }, + schema: 'group', + }, + ], + }, + }; +}; + +export const getOpenVsClosePanel = (): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + '5': { + gridData: { + w: 48, + h: 12, + x: 0, + y: 0, + i: '5', + }, + type: 'visualization', + explicitInput: { + id: '5', + savedVis: getVisStateOpenVsCloseVulnerabilities('wazuh-alerts-*'), + }, + }, + }; +}; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-filters.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-filters.ts new file mode 100644 index 0000000000..8e25e649f5 --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-filters.ts @@ -0,0 +1,159 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; +import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; + +const getVisStateFilter = ( + id: string, + indexPatternId: string, + title: string, + label: string, + fieldName: string, +) => { + return { + id, + title, + type: 'table', + params: { + perPage: 5, + percentageCol: '', + row: true, + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + totalFunc: 'sum', + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: 'Count', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: fieldName, + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: label, + }, + schema: 'bucket', + }, + ], + }, + }; +}; + +export const getDashboardFilters = (): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + topPackageSelector: { + gridData: { + w: 12, + h: 12, + x: 0, + y: 0, + i: 'topPackageSelector', + }, + type: 'visualization', + explicitInput: { + id: 'topPackageSelector', + savedVis: getVisStateFilter( + 'topPackageSelector', + VULNERABILITIES_INDEX_PATTERN_ID, + 'Top Packages vulnerabilities', + 'Package', + 'package.name', + ), + }, + }, + topOSVulnerabilities: { + gridData: { + w: 12, + h: 12, + x: 12, + y: 0, + i: 'topOSVulnerabilities', + }, + type: 'visualization', + explicitInput: { + id: 'topOSVulnerabilities', + savedVis: getVisStateFilter( + 'topOSVulnerabilities', + VULNERABILITIES_INDEX_PATTERN_ID, + 'Top Operating system vulnerabilities', + 'Operating system', + 'host.os.name', + ), + }, + }, + topAgentVulnerabilities: { + gridData: { + w: 12, + h: 12, + x: 24, + y: 0, + i: 'topAgentVulnerabilities', + }, + type: 'visualization', + explicitInput: { + id: 'topAgentVulnerabilities', + savedVis: getVisStateFilter( + 'topAgentVulnerabilities', + VULNERABILITIES_INDEX_PATTERN_ID, + 'Agent filter', + 'Agent', + 'agent.id', + ), + }, + }, + topVulnerabilities: { + gridData: { + w: 12, + h: 12, + x: 36, + y: 0, + i: 'topVulnerabilities', + }, + type: 'visualization', + explicitInput: { + id: 'topVulnerabilities', + savedVis: getVisStateFilter( + 'topVulnerabilities', + VULNERABILITIES_INDEX_PATTERN_ID, + 'Top vulnerabilities', + 'Vulnerability', + 'vulnerability.id', + ), + }, + }, + }; +}; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-kpis.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-kpis.ts new file mode 100644 index 0000000000..d7950c46bd --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-kpis.ts @@ -0,0 +1,415 @@ +import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; +import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; +import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; + +const getVisStateSeverityCritical = (indexPatternId: string) => { + return { + id: 'severity_critical_vulnerabilities', + title: 'Critical', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Reds', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 50, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: ' ', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'vulnerability.severity:"critical"', + language: 'kuery', + }, + label: '- Critical Severity Alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateSeverityHigh = (indexPatternId: string) => { + return { + id: 'severity_high_vulnerabilities', + title: 'High', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Blues', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 50, + }, + }, + }, + uiState: { + vis: { + colors: { + 'High Severity Alerts - Count': '#38D1BA', + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: ' ', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'vulnerability.severity:"high"', + language: 'kuery', + }, + label: '- High Severity Alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateSeverityMedium = (indexPatternId: string) => { + return { + id: 'severity_medium_vulnerabilities', + title: 'Medium', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Yellow to Red', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: true, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 50, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: ' ', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'vulnerability.severity:"medium"', + language: 'kuery', + }, + label: '- Medium Severity Alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +const getVisStateSeverityLow = (indexPatternId: string) => { + return { + id: 'severity_low_vulnerabilities', + title: 'Low', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 50, + }, + }, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: ' ', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ + { + input: { + query: 'vulnerability.severity:"low"', + language: 'kuery', + }, + label: '- Low Severity Alerts', + }, + ], + }, + schema: 'group', + }, + ], + }, + }; +}; + +export const getKPIsPanel = (): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + '1': { + gridData: { + w: 12, + h: 6, + x: 0, + y: 0, + i: '1', + }, + type: 'visualization', + explicitInput: { + id: '1', + savedVis: getVisStateSeverityCritical(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + '2': { + gridData: { + w: 12, + h: 6, + x: 12, + y: 0, + i: '2', + }, + type: 'visualization', + explicitInput: { + id: '2', + savedVis: getVisStateSeverityHigh(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + '3': { + gridData: { + w: 12, + h: 6, + x: 24, + y: 0, + i: '3', + }, + type: 'visualization', + explicitInput: { + id: '3', + savedVis: getVisStateSeverityMedium(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + '4': { + gridData: { + w: 12, + h: 6, + x: 36, + y: 0, + i: '4', + }, + type: 'visualization', + explicitInput: { + id: '4', + savedVis: getVisStateSeverityLow(VULNERABILITIES_INDEX_PATTERN_ID), + }, + }, + }; +}; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts index 424dc48278..08613f37ed 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts @@ -2,353 +2,6 @@ import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboa import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; -const getVisStateSeverityCritical = (indexPatternId: string) => { - return { - id: 'severity_critical_vulnerabilities', - title: 'Critical', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Reds', - metricColorMode: 'Labels', - colorsRange: [ - { - from: 0, - to: 0, - }, - { - from: 0, - to: 0, - }, - ], - labels: { - show: true, - }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 50, - }, - }, - }, - data: { - searchSource: { - query: { - language: 'kuery', - query: '', - }, - filter: [], - index: indexPatternId, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: indexPatternId, - }, - ], - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: { - customLabel: ' ', - }, - schema: 'metric', - }, - { - id: '2', - enabled: true, - type: 'filters', - params: { - filters: [ - { - input: { - query: 'vulnerability.severity:"critical"', - language: 'kuery', - }, - label: '- Critical Severity Alerts', - }, - ], - }, - schema: 'group', - }, - ], - }, - }; -}; - -const getVisStateSeverityHigh = (indexPatternId: string) => { - return { - id: 'severity_high_vulnerabilities', - title: 'High', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Blues', - metricColorMode: 'Labels', - colorsRange: [ - { - from: 0, - to: 0, - }, - { - from: 0, - to: 0, - }, - ], - labels: { - show: true, - }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 50, - }, - }, - }, - uiState: { - vis: { - colors: { - 'High Severity Alerts - Count': '#38D1BA', - }, - }, - }, - data: { - searchSource: { - query: { - language: 'kuery', - query: '', - }, - filter: [], - index: indexPatternId, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: indexPatternId, - }, - ], - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: { - customLabel: ' ', - }, - schema: 'metric', - }, - { - id: '2', - enabled: true, - type: 'filters', - params: { - filters: [ - { - input: { - query: 'vulnerability.severity:"high"', - language: 'kuery', - }, - label: '- High Severity Alerts', - }, - ], - }, - schema: 'group', - }, - ], - }, - }; -}; - -const getVisStateSeverityMedium = (indexPatternId: string) => { - return { - id: 'severity_medium_vulnerabilities', - title: 'Medium', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Yellow to Red', - metricColorMode: 'Labels', - colorsRange: [ - { - from: 0, - to: 0, - }, - { - from: 0, - to: 0, - }, - ], - labels: { - show: true, - }, - invertColors: true, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 50, - }, - }, - }, - data: { - searchSource: { - query: { - language: 'kuery', - query: '', - }, - filter: [], - index: indexPatternId, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: indexPatternId, - }, - ], - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: { - customLabel: ' ', - }, - schema: 'metric', - }, - { - id: '2', - enabled: true, - type: 'filters', - params: { - filters: [ - { - input: { - query: 'vulnerability.severity:"medium"', - language: 'kuery', - }, - label: '- Medium Severity Alerts', - }, - ], - }, - schema: 'group', - }, - ], - }, - }; -}; - -const getVisStateSeverityLow = (indexPatternId: string) => { - return { - id: 'severity_low_vulnerabilities', - title: 'Low', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Greens', - metricColorMode: 'Labels', - colorsRange: [ - { - from: 0, - to: 0, - }, - { - from: 0, - to: 0, - }, - ], - labels: { - show: true, - }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 50, - }, - }, - }, - data: { - searchSource: { - query: { - language: 'kuery', - query: '', - }, - filter: [], - index: indexPatternId, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: indexPatternId, - }, - ], - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: { - customLabel: ' ', - }, - schema: 'metric', - }, - { - id: '2', - enabled: true, - type: 'filters', - params: { - filters: [ - { - input: { - query: 'vulnerability.severity:"low"', - language: 'kuery', - }, - label: '- Low Severity Alerts', - }, - ], - }, - schema: 'group', - }, - ], - }, - }; -}; - const getVisStateTopVulnerabilities = (indexPatternId: string) => { return { id: 'most_detected_vulnerabilities', @@ -916,245 +569,6 @@ const getVisStateInventoryTable = (indexPatternId: string) => { }; }; -const getVisStateOpenVsCloseVulnerabilities = (indexPatternId: string) => { - return { - id: 'open_vs_close_vulnerabilities', - title: 'Open vs. Close', - type: 'line', - params: { - type: 'line', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { - type: 'linear', - }, - labels: { - show: true, - filter: true, - truncate: 100, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { - type: 'linear', - mode: 'normal', - }, - labels: { - show: true, - rotate: 0, - filter: false, - truncate: 100, - }, - title: { - text: 'Count', - }, - }, - ], - seriesParams: [ - { - show: true, - type: 'line', - mode: 'normal', - data: { - label: 'Count', - id: '1', - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - interpolate: 'linear', - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: 'full', - color: '#E7664C', - }, - }, - data: { - searchSource: { - query: { - language: 'kuery', - query: '', - }, - filter: [], - index: indexPatternId, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: indexPatternId, - }, - ], - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: { - customLabel: 'Count', - }, - schema: 'metric', - }, - { - id: '2', - enabled: true, - type: 'date_histogram', - params: { - field: 'timestamp', - timeRange: { - from: 'now-1d', - to: 'now', - }, - useNormalizedOpenSearchInterval: true, - scaleMetricValues: false, - interval: 'auto', - // eslint-disable-next-line camelcase - drop_partials: false, - // eslint-disable-next-line camelcase - min_doc_count: 1, - // eslint-disable-next-line camelcase - extended_bounds: {}, - }, - schema: 'segment', - }, - { - id: '3', - enabled: true, - type: 'terms', - params: { - field: 'data.vulnerability.state', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - exclude: 'Pending confirmation', - }, - schema: 'group', - }, - ], - }, - }; -}; - -export const getKPIsPanel = (): { - [panelId: string]: DashboardPanelState< - EmbeddableInput & { [k: string]: unknown } - >; -} => { - return { - '1': { - gridData: { - w: 12, - h: 6, - x: 0, - y: 0, - i: '1', - }, - type: 'visualization', - explicitInput: { - id: '1', - savedVis: getVisStateSeverityCritical(VULNERABILITIES_INDEX_PATTERN_ID), - }, - }, - '2': { - gridData: { - w: 12, - h: 6, - x: 12, - y: 0, - i: '2', - }, - type: 'visualization', - explicitInput: { - id: '2', - savedVis: getVisStateSeverityHigh(VULNERABILITIES_INDEX_PATTERN_ID), - }, - }, - '3': { - gridData: { - w: 12, - h: 6, - x: 24, - y: 0, - i: '3', - }, - type: 'visualization', - explicitInput: { - id: '3', - savedVis: getVisStateSeverityMedium(VULNERABILITIES_INDEX_PATTERN_ID), - }, - }, - '4': { - gridData: { - w: 12, - h: 6, - x: 36, - y: 0, - i: '4', - }, - type: 'visualization', - explicitInput: { - id: '4', - savedVis: getVisStateSeverityLow(VULNERABILITIES_INDEX_PATTERN_ID), - }, - }, - }; -}; - -export const getOpenVsClosePanel = (): { - [panelId: string]: DashboardPanelState< - EmbeddableInput & { [k: string]: unknown } - >; -} => { - return { - '5': { - gridData: { - w: 48, - h: 12, - x: 0, - y: 0, - i: '5', - }, - type: 'visualization', - explicitInput: { - id: '5', - savedVis: getVisStateOpenVsCloseVulnerabilities('wazuh-alerts-*'), - }, - }, - }; -}; - export const getDashboardPanels = (): { [panelId: string]: DashboardPanelState< EmbeddableInput & { [k: string]: unknown } diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx index f2cd69425e..c4b16d3697 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx @@ -1,16 +1,14 @@ import React from 'react'; import { getPlugins } from '../../../../../kibana-services'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; -import { - getDashboardPanels, - getKPIsPanel, - getOpenVsClosePanel, -} from './dashboard-panels'; +import { getDashboardPanels } from './dashboard-panels'; import { I18nProvider } from '@osd/i18n/react'; import useSearchBarConfiguration from '../../searchbar/use-search-bar-configuration'; import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; -import { DashboardFilters } from './dashboard-filters/dashboard-filters'; - +import { getDashboardFilters } from './dashboard-panels-filters'; +import './vulnerability-detector-filters.scss'; +import { getKPIsPanel } from './dashboard-panels-kpis'; +import { getOpenVsClosePanel } from './dashboard-panel-open-vs-close'; const plugins = getPlugins(); const SearchBar = getPlugins().data.ui.SearchBar; @@ -35,7 +33,30 @@ export const DashboardVuls: React.FC = () => { {...searchBarProps} /> - +
+ +
{ value: 15, }, hidePanelTitles: false, - disableTriggers: true, }} /> { value: 15, }, hidePanelTitles: false, - disableTriggers: true, }} /> diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/vulnerability-detector-filters.scss b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/vulnerability-detector-filters.scss similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-filters/vulnerability-detector-filters.scss rename to plugins/main/public/components/overview/vulnerabilities/dashboards/overview/vulnerability-detector-filters.scss From 68686f0b295a92da4f9bc40ed8aa588ba52a93d2 Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Mon, 30 Oct 2023 16:55:38 -0300 Subject: [PATCH 12/20] Remove date picker from searchbar on vuls inventory tab --- .../dashboards/inventory/config/index.ts | 4 - .../dashboards/inventory/inventory.tsx | 141 +++++++++++++----- .../data_grid/use_data_grid.ts | 3 +- 3 files changed, 105 insertions(+), 43 deletions(-) diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts index fce980ecad..2350e417cf 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts @@ -1,10 +1,6 @@ import { EuiDataGridColumn } from "@elastic/eui"; export const inventoryTableDefaultColumns: EuiDataGridColumn[] = [ - { - id: '@timestamp', - displayAsText: 'Timestamp', - }, { id: 'package.name', displayAsText: 'Name', diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx index 717beef298..c2519edb22 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx @@ -14,6 +14,7 @@ import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle, + EuiButtonEmpty, } from '@elastic/eui'; import { Filter, IndexPattern, OpenSearchQuerySortValue } from '../../../../../../../../src/plugins/data/common'; import { SearchResponse } from '../../../../../../../../src/core/server'; @@ -25,6 +26,7 @@ import { inventoryTableDefaultColumns } from './config'; import { useDocViewer } from '../../doc_viewer/use_doc_viewer'; import './inventory.scss'; import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; +import * as FileSaver from '../../../../../services/file-saver'; export const InventoryVuls = () => { const { searchBarProps } = useSearchBarConfiguration({ @@ -65,7 +67,7 @@ export const InventoryVuls = () => { DocViewInspectButton }) - const { pagination, sorting } = dataGridProps; + const { pagination, sorting, columnVisibility } = dataGridProps; const docViewerProps = useDocViewer({ doc: inspectedHit, @@ -76,7 +78,16 @@ export const InventoryVuls = () => { if (!isLoading) { setIndexPattern(indexPatterns?.[0] as IndexPattern); try { - search(); + search({ + indexPattern: indexPatterns?.[0] as IndexPattern, + filters, + query, + pagination, + sorting + }).then((results) => { + setResults(results); + setIsSearching(false); + }); }catch(error){ console.error(error); // check when filters are wrong and the search fails @@ -87,44 +98,77 @@ export const InventoryVuls = () => { /** * Search in index pattern */ - const search = async (): Promise => { - const indexPattern = indexPatterns?.[0]; - if (indexPattern) { - setIsSearching(true); - const data = getPlugins().data - const searchSource = await data.search.searchSource.create(); - const timeFilter: Filter['query'] = [{ - range: { - '@timestamp': { - gte: searchBarProps?.dateRangeFrom, - lte: searchBarProps?.dateRangeTo, - format: 'strict_date_optional_time', - }, - }, - }] - const combined = [...timeFilter, ...(filters || [])]; - const fromField = (pagination?.pageIndex || 0) * (pagination?.pageSize || 100); - const sortOrder: OpenSearchQuerySortValue[] = sorting?.columns.map((column) => { - const sortDirection = column.direction === 'asc' ? 'asc' : 'desc'; - return { [column?.id || '']: sortDirection } as OpenSearchQuerySortValue; - }) || []; - - const results = await searchSource - .setParent(undefined) - .setField('filter', combined) - .setField('query', query) - .setField('sort', sortOrder) - .setField('size', pagination?.pageSize) - .setField('from', fromField) - .setField('index', indexPattern as IndexPattern) - .fetch(); - setResults(results); - setIsSearching(false); - } + interface SearchParams { + indexPattern: IndexPattern; + filters?: Filter[]; + query?: any; + pagination?: { + pageIndex?: number; + pageSize?: number; + }; + fields?: string[], + sorting?: { + columns: { + id: string; + direction: 'asc' | 'desc'; + }[]; + }; + } + + const search = async (params: SearchParams): Promise => { + const { indexPattern, filters = [], query, pagination, sorting, fields } = params; + const data = getPlugins().data; + const searchSource = await data.search.searchSource.create(); + const fromField = (pagination?.pageIndex || 0) * (pagination?.pageSize || 100); + const sortOrder: OpenSearchQuerySortValue[] = sorting?.columns.map((column) => { + const sortDirection = column.direction === 'asc' ? 'asc' : 'desc'; + return { [column?.id || '']: sortDirection } as OpenSearchQuerySortValue; + }) || []; + + const searchParams = searchSource + .setParent(undefined) + .setField('filter', filters) + .setField('query', query) + .setField('sort', sortOrder) + .setField('size', pagination?.pageSize) + .setField('from', fromField) + .setField('index', indexPattern) + + // add fields + if(fields && Array.isArray(fields) && fields.length > 0) + searchParams.setField('fields',fields); + + + return await searchParams.fetch(); }; const timeField = indexPattern?.timeFieldName ? indexPattern.timeFieldName : undefined; + const onClickExport = async () => { + // use the search method to get the data and pass the results.hits.total to make + + const result = await search({ + indexPattern: indexPatterns?.[0] as IndexPattern, + filters, + query, + fields: columnVisibility.visibleColumns, + pagination: { + pageIndex: 0, + pageSize: results.hits.total + }, + sorting + }) + + const allResults = result.hits.hits.map((hit: any) => { + return indexPattern?.flattenHit(hit); + }); + + const blob = new Blob(allResults, { type: 'text/csv' }); + FileSaver.saveAs(blob, `algo.csv`); + // use indexPattern to transform hits to object similar to flatenned method + + } + return ( @@ -137,13 +181,34 @@ export const InventoryVuls = () => { <> {isLoading ? : - } + } {isSearching ? : null} {!isLoading && !isSearching && results?.hits?.total === 0 ? : null} {!isLoading && !isSearching && results?.hits?.total > 0 ? - : null} + + Export Formated + + ), + showFullScreenSelector: false + }} + /> : null} {inspectedHit && ( setInspectedHit(undefined)} size="m"> diff --git a/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts b/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts index 7ef7cc50e0..a31cb1b7bd 100644 --- a/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts +++ b/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts @@ -1,5 +1,5 @@ import { EuiDataGridCellValueElementProps, EuiDataGridColumn, EuiDataGridProps, EuiDataGridSorting } from "@elastic/eui" -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, Fragment } from "react"; import { SearchResponse } from "@opensearch-project/opensearch/api/types"; import { IFieldType, IndexPattern } from "../../../../../../../src/plugins/data/common"; @@ -14,6 +14,7 @@ type tDataGridProps = { export const parseColumns = (fields: IFieldType[]): EuiDataGridColumn[] => { return fields.map((field) => { return { + ...field, id: field.name, display: field.name, schema: field.type, From f583dd3e6316b294d596b43c5131b457f7516bad Mon Sep 17 00:00:00 2001 From: jbiset Date: Mon, 30 Oct 2023 18:03:16 -0300 Subject: [PATCH 13/20] Remove date picker from searchbar and open vs close visualization on vuls dashboard tab --- .../overview/dashboard-panel-open-vs-close.ts | 176 ------------------ .../dashboards/overview/dashboard.tsx | 31 +-- 2 files changed, 5 insertions(+), 202 deletions(-) delete mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panel-open-vs-close.ts diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panel-open-vs-close.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panel-open-vs-close.ts deleted file mode 100644 index aaceb7af69..0000000000 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panel-open-vs-close.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { DashboardPanelState } from '../../../../../../../../src/plugins/dashboard/public/application'; -import { EmbeddableInput } from '../../../../../../../../src/plugins/embeddable/public'; - -const getVisStateOpenVsCloseVulnerabilities = (indexPatternId: string) => { - return { - id: 'open_vs_close_vulnerabilities', - title: 'Open vs. Close', - type: 'line', - params: { - type: 'line', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { - type: 'linear', - }, - labels: { - show: true, - filter: true, - truncate: 100, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { - type: 'linear', - mode: 'normal', - }, - labels: { - show: true, - rotate: 0, - filter: false, - truncate: 100, - }, - title: { - text: 'Count', - }, - }, - ], - seriesParams: [ - { - show: true, - type: 'line', - mode: 'normal', - data: { - label: 'Count', - id: '1', - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - interpolate: 'linear', - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: 'full', - color: '#E7664C', - }, - }, - data: { - searchSource: { - query: { - language: 'kuery', - query: '', - }, - filter: [], - index: indexPatternId, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: indexPatternId, - }, - ], - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: { - customLabel: 'Count', - }, - schema: 'metric', - }, - { - id: '2', - enabled: true, - type: 'date_histogram', - params: { - field: 'timestamp', - timeRange: { - from: 'now-1d', - to: 'now', - }, - useNormalizedOpenSearchInterval: true, - scaleMetricValues: false, - interval: 'auto', - // eslint-disable-next-line camelcase - drop_partials: false, - // eslint-disable-next-line camelcase - min_doc_count: 1, - // eslint-disable-next-line camelcase - extended_bounds: {}, - }, - schema: 'segment', - }, - { - id: '3', - enabled: true, - type: 'terms', - params: { - field: 'data.vulnerability.state', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - exclude: 'Pending confirmation', - }, - schema: 'group', - }, - ], - }, - }; -}; - -export const getOpenVsClosePanel = (): { - [panelId: string]: DashboardPanelState< - EmbeddableInput & { [k: string]: unknown } - >; -} => { - return { - '5': { - gridData: { - w: 48, - h: 12, - x: 0, - y: 0, - i: '5', - }, - type: 'visualization', - explicitInput: { - id: '5', - savedVis: getVisStateOpenVsCloseVulnerabilities('wazuh-alerts-*'), - }, - }, - }; -}; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx index c4b16d3697..f028ddf770 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx @@ -8,7 +8,6 @@ import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; import { getDashboardFilters } from './dashboard-panels-filters'; import './vulnerability-detector-filters.scss'; import { getKPIsPanel } from './dashboard-panels-kpis'; -import { getOpenVsClosePanel } from './dashboard-panel-open-vs-close'; const plugins = getPlugins(); const SearchBar = getPlugins().data.ui.SearchBar; @@ -29,11 +28,14 @@ export const DashboardVuls: React.FC = () => { <> -
+
{ hidePanelTitles: true, }} /> - Date: Tue, 31 Oct 2023 11:49:34 -0300 Subject: [PATCH 14/20] Change Accumulation of the most detected vulnerabilities chart --- .../dashboards/overview/dashboard-panels.ts | 118 ++++++++++++------ 1 file changed, 79 insertions(+), 39 deletions(-) diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts index 08613f37ed..70dc718d03 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts @@ -296,48 +296,87 @@ const getVisStateTopVulnerabilitiesEndpoints = (indexPatternId: string) => { }; }; -const getVisStateAccumulationMostDetectedVulnerabilities = ( - indexPatternId: string, -) => { +const getVisStateAccumulationMostDetectedVulnerabilities = (indexPatternId: string) => { return { id: 'accumulation_most_vulnerable_vulnerabilities', title: 'Accumulation of the most detected vulnerabilities', - type: 'heatmap', + type: 'line', params: { - addLegend: true, - addTooltip: true, - colorSchema: 'Greens', - colorsNumber: 5, - colorsRange: [ + type: 'line', + grid: { + categoryLines: false, + }, + categoryAxes: [ { - from: 0, - to: 100, + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, }, ], - enableHover: false, - invertColors: false, - legendPosition: 'right', - percentageMode: false, - setColorRange: false, - times: [], - type: 'heatmap', valueAxes: [ { id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, labels: { - color: 'black', - overwriteColor: false, + show: true, rotate: 0, - show: false, + filter: false, + truncate: 100, }, - scale: { - defaultYExtents: false, - type: 'linear', + title: { + text: 'Count', }, - show: false, - type: 'value', }, ], + seriesParams: [ + { + show: true, + type: 'line', + mode: 'normal', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: false, + lineWidth: 2, + interpolate: 'linear', + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + radiusRatio: 20, }, data: { searchSource: { @@ -366,6 +405,13 @@ const getVisStateAccumulationMostDetectedVulnerabilities = ( { id: '2', enabled: true, + type: 'count', + params: {}, + schema: 'radius', + }, + { + id: '4', + enabled: true, type: 'terms', params: { field: 'vulnerability.id', @@ -373,7 +419,7 @@ const getVisStateAccumulationMostDetectedVulnerabilities = ( order: 'desc', size: 5, otherBucket: false, - otherBucketLabel: 'Other', + otherBucketLabel: 'Others', missingBucket: false, missingBucketLabel: 'Missing', }, @@ -384,9 +430,9 @@ const getVisStateAccumulationMostDetectedVulnerabilities = ( enabled: true, type: 'date_histogram', params: { - field: '@timestamp', + field: 'event.created', timeRange: { - from: 'now-90d', + from: 'now-24h', to: 'now', }, useNormalizedOpenSearchInterval: true, @@ -570,9 +616,7 @@ const getVisStateInventoryTable = (indexPatternId: string) => { }; export const getDashboardPanels = (): { - [panelId: string]: DashboardPanelState< - EmbeddableInput & { [k: string]: unknown } - >; + [panelId: string]: DashboardPanelState; } => { return { '6': { @@ -586,9 +630,7 @@ export const getDashboardPanels = (): { type: 'visualization', explicitInput: { id: '6', - savedVis: getVisStateTopVulnerabilities( - VULNERABILITIES_INDEX_PATTERN_ID, - ), + savedVis: getVisStateTopVulnerabilities(VULNERABILITIES_INDEX_PATTERN_ID), }, }, '7': { @@ -602,9 +644,7 @@ export const getDashboardPanels = (): { type: 'visualization', explicitInput: { id: '7', - savedVis: getVisStateTopVulnerabilitiesEndpoints( - VULNERABILITIES_INDEX_PATTERN_ID, - ), + savedVis: getVisStateTopVulnerabilitiesEndpoints(VULNERABILITIES_INDEX_PATTERN_ID), }, }, '8': { @@ -619,7 +659,7 @@ export const getDashboardPanels = (): { explicitInput: { id: '8', savedVis: getVisStateAccumulationMostDetectedVulnerabilities( - VULNERABILITIES_INDEX_PATTERN_ID, + VULNERABILITIES_INDEX_PATTERN_ID ), }, }, From 457917ad4bfd9b3d261ed3033a4fc30297bc7800 Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Tue, 31 Oct 2023 16:02:04 -0300 Subject: [PATCH 15/20] Add data grid csv export --- .../dashboards/inventory/config/index.ts | 8 - .../dashboards/inventory/inventory.scss | 8 +- .../dashboards/inventory/inventory.tsx | 68 +------- .../dashboards/inventory/inventory_service.ts | 153 ++++++++++++++++++ .../data_grid/use_data_grid.ts | 38 +---- 5 files changed, 170 insertions(+), 105 deletions(-) create mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory_service.ts diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts index 2350e417cf..eb2d9d2c3a 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/config/index.ts @@ -3,34 +3,26 @@ import { EuiDataGridColumn } from "@elastic/eui"; export const inventoryTableDefaultColumns: EuiDataGridColumn[] = [ { id: 'package.name', - displayAsText: 'Name', }, { id: 'package.version', - displayAsText: 'Version', }, { id: 'package.architecture', - displayAsText: 'Architecture', }, { id: 'vulnerability.severity', - displayAsText: 'Severity', }, { id: 'vulnerability.id', - displayAsText: 'Id', }, { id: 'vulnerability.score.version', - displayAsText: 'Score version', }, { id: 'vulnerability.score.base', - displayAsText: 'Score', }, { id: 'event.created', - displayAsText: 'Detected time', } ] \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.scss b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.scss index df8c71901f..e3329465a8 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.scss +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.scss @@ -5,4 +5,10 @@ .headerIsExpanded .vulsInventoryContainer { height: calc(100vh - 153px); -} \ No newline at end of file +} + +.vulsInventoryContainer .euiDataGrid--fullScreen { + height: calc(100vh - 49px); + bottom: 0; + top: auto; +} diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx index c2519edb22..2b9634af9a 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx @@ -16,7 +16,7 @@ import { EuiTitle, EuiButtonEmpty, } from '@elastic/eui'; -import { Filter, IndexPattern, OpenSearchQuerySortValue } from '../../../../../../../../src/plugins/data/common'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { SearchResponse } from '../../../../../../../../src/core/server'; import DocViewer from '../../doc_viewer/doc_viewer'; import { DiscoverNoResults } from '../../common/components/no_results'; @@ -26,7 +26,7 @@ import { inventoryTableDefaultColumns } from './config'; import { useDocViewer } from '../../doc_viewer/use_doc_viewer'; import './inventory.scss'; import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; -import * as FileSaver from '../../../../../services/file-saver'; +import { search, exportSearchToCSV } from './inventory_service'; export const InventoryVuls = () => { const { searchBarProps } = useSearchBarConfiguration({ @@ -95,59 +95,14 @@ export const InventoryVuls = () => { } }, [JSON.stringify(searchBarProps), JSON.stringify(pagination), JSON.stringify(sorting)]); - /** - * Search in index pattern - */ - interface SearchParams { - indexPattern: IndexPattern; - filters?: Filter[]; - query?: any; - pagination?: { - pageIndex?: number; - pageSize?: number; - }; - fields?: string[], - sorting?: { - columns: { - id: string; - direction: 'asc' | 'desc'; - }[]; - }; - } - const search = async (params: SearchParams): Promise => { - const { indexPattern, filters = [], query, pagination, sorting, fields } = params; - const data = getPlugins().data; - const searchSource = await data.search.searchSource.create(); - const fromField = (pagination?.pageIndex || 0) * (pagination?.pageSize || 100); - const sortOrder: OpenSearchQuerySortValue[] = sorting?.columns.map((column) => { - const sortDirection = column.direction === 'asc' ? 'asc' : 'desc'; - return { [column?.id || '']: sortDirection } as OpenSearchQuerySortValue; - }) || []; - - const searchParams = searchSource - .setParent(undefined) - .setField('filter', filters) - .setField('query', query) - .setField('sort', sortOrder) - .setField('size', pagination?.pageSize) - .setField('from', fromField) - .setField('index', indexPattern) - - // add fields - if(fields && Array.isArray(fields) && fields.length > 0) - searchParams.setField('fields',fields); - - - return await searchParams.fetch(); - }; const timeField = indexPattern?.timeFieldName ? indexPattern.timeFieldName : undefined; - const onClickExport = async () => { + const onClickExportResults = async () => { // use the search method to get the data and pass the results.hits.total to make - const result = await search({ + const params = { indexPattern: indexPatterns?.[0] as IndexPattern, filters, query, @@ -157,16 +112,9 @@ export const InventoryVuls = () => { pageSize: results.hits.total }, sorting - }) - - const allResults = result.hits.hits.map((hit: any) => { - return indexPattern?.flattenHit(hit); - }); - - const blob = new Blob(allResults, { type: 'text/csv' }); - FileSaver.saveAs(blob, `algo.csv`); - // use indexPattern to transform hits to object similar to flatenned method + } + await exportSearchToCSV(params); } @@ -198,15 +146,15 @@ export const InventoryVuls = () => { toolbarVisibility={{ additionalControls: ( + onClick={onClickExportResults}> Export Formated ), - showFullScreenSelector: false }} /> : null} {inspectedHit && ( diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory_service.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory_service.ts new file mode 100644 index 0000000000..645d9ff0dd --- /dev/null +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory_service.ts @@ -0,0 +1,153 @@ +import { SearchResponse } from "../../../../../../../../src/core/server"; +import { getPlugins } from '../../../../../kibana-services'; +import { IndexPattern, Filter, OpenSearchQuerySortValue } from "../../../../../../../../src/plugins/data/public"; +import * as FileSaver from '../../../../../services/file-saver'; +import { beautifyDate } from "../../../../agents/vuls/inventory/lib"; + + +interface SearchParams { + indexPattern: IndexPattern; + filters?: Filter[]; + query?: any; + pagination?: { + pageIndex?: number; + pageSize?: number; + }; + fields?: string[], + sorting?: { + columns: { + id: string; + direction: 'asc' | 'desc'; + }[]; + }; +} + + +export const search = async (params: SearchParams): Promise => { + const { indexPattern, filters = [], query, pagination, sorting, fields } = params; + const data = getPlugins().data; + const searchSource = await data.search.searchSource.create(); + const fromField = (pagination?.pageIndex || 0) * (pagination?.pageSize || 100); + const sortOrder: OpenSearchQuerySortValue[] = sorting?.columns.map((column) => { + const sortDirection = column.direction === 'asc' ? 'asc' : 'desc'; + return { [column?.id || '']: sortDirection } as OpenSearchQuerySortValue; + }) || []; + + const searchParams = searchSource + .setParent(undefined) + .setField('filter', filters) + .setField('query', query) + .setField('sort', sortOrder) + .setField('size', pagination?.pageSize) + .setField('from', fromField) + .setField('index', indexPattern) + + // add fields + if (fields && Array.isArray(fields) && fields.length > 0) + searchParams.setField('fields', fields); + + + return await searchParams.fetch(); +}; + + +export const parseData = (resultsHits: SearchResponse['hits']['hits']): any[] => { + const data = resultsHits.map((hit) => { + if (!hit) { + return {} + } + const source = hit._source as object; + const data = { + ...source, + _id: hit._id, + _index: hit._index, + _type: hit._type, + _score: hit._score, + }; + return data; + }); + return data; +} + + +export const getFieldFormatted = (rowIndex, columnId, indexPattern, rowsParsed) => { + const field = indexPattern.fields.find((field) => field.name === columnId); + let fieldValue = null; + if (columnId.includes('.')) { + // when the column is a nested field. The column could have 2 to n levels + // get dinamically the value of the nested field + const nestedFields = columnId.split('.'); + fieldValue = rowsParsed[rowIndex]; + nestedFields.forEach((field) => { + if (fieldValue) { + fieldValue = fieldValue[field]; + } + }); + + } else { + fieldValue = rowsParsed[rowIndex][columnId].formatted + ? rowsParsed[rowIndex][columnId].formatted + : rowsParsed[rowIndex][columnId]; + } + + // if is date field + if (field?.type === 'date') { + // @ts-ignore + fieldValue = beautifyDate(fieldValue); + } + return fieldValue; +} + +export const exportSearchToCSV = async (params: SearchParams): Promise => { + const { indexPattern, filters = [], query, sorting, fields } = params; + const searchResults = await search(params); + const resultsFields = fields; + const data = searchResults.hits.hits.map((hit) => { + // check if the field type is a date + const dateFields = indexPattern.fields.getByType('date'); + const dateFieldsNames = dateFields.map((field) => field.name); + const flattenHit = indexPattern.flattenHit(hit); + // replace the date fields with the formatted date + dateFieldsNames.forEach((field) => { + if (flattenHit[field]) { + flattenHit[field] = beautifyDate(flattenHit[field]); + } + }); + return flattenHit; + }); + + if (!resultsFields || resultsFields.length === 0){ + return; + } + + if (!data || data.length === 0) + return; + + const parsedData = data.map((row) => { + const parsedRow = resultsFields?.map((field) => { + const value = row[field]; + if (value === undefined || value === null) { + return ''; + } + if (typeof value === 'object') { + return JSON.stringify(value); + } + return `"${value}"`; + }); + return parsedRow?.join(','); + }).join('\n'); + + + + // create a csv file using blob + const blobData = new Blob( + [ + `${resultsFields?.join(',')}\n${parsedData}` + ], + { type: 'text/csv' } + ); + + if (blobData) { + FileSaver?.saveAs(blobData, 'vulnerabilities_inventory.csv'); + } +} \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts b/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts index a31cb1b7bd..4142d0c0f2 100644 --- a/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts +++ b/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts @@ -2,6 +2,7 @@ import { EuiDataGridCellValueElementProps, EuiDataGridColumn, EuiDataGridProps, import { useEffect, useMemo, useState, Fragment } from "react"; import { SearchResponse } from "@opensearch-project/opensearch/api/types"; import { IFieldType, IndexPattern } from "../../../../../../../src/plugins/data/common"; +import { parseData, getFieldFormatted } from '../dashboards/inventory/inventory_service'; type tDataGridProps = { indexPattern: IndexPattern; @@ -63,49 +64,14 @@ export const useDataGrid = (props: tDataGridProps): EuiDataGridProps => { setPagination((pagination) => ({ ...pagination, pageIndex: 0 })); }, [rowCount]) - const parseData = (resultsHits: SearchResponse['hits']['hits']): any[] => { - const data = resultsHits.map((hit) => { - if (!hit) { - return {} - } - const source = hit._source as object; - const data = { - ...source, - _id: hit._id, - _index: hit._index, - _type: hit._type, - _score: hit._score, - }; - return data; - }); - return data; - } const renderCellValue = ({ rowIndex, columnId, setCellProps }) => { const rowsParsed = parseData(rows); - function getFormatted(rowIndex, columnId) { - if (columnId.includes('.')) { - // when the column is a nested field. The column could have 2 to n levels - // get dinamically the value of the nested field - const nestedFields = columnId.split('.'); - let value = rowsParsed[rowIndex]; - nestedFields.forEach((field) => { - if (value) { - value = value[field]; - } - }); - return value; - } else { - return rowsParsed[rowIndex][columnId].formatted - ? rowsParsed[rowIndex][columnId].formatted - : rowsParsed[rowIndex][columnId]; - } - } // On the context data always is stored the current page data (pagination) // then the rowIndex is relative to the current page const relativeRowIndex = rowIndex % pagination.pageSize; return rowsParsed.hasOwnProperty(relativeRowIndex) - ? getFormatted(relativeRowIndex, columnId) + ? getFieldFormatted(relativeRowIndex, columnId, indexPattern, rowsParsed) : null; }; From edb04e27e343cc4a39ba480aac373f33b1b2953d Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Wed, 1 Nov 2023 16:53:48 -0300 Subject: [PATCH 16/20] Add error management and export csv calls pagination --- ...ts => use_dashboard_configuration.test.ts} | 2 +- ...on.tsx => use_dashboard_configuration.tsx} | 0 .../dashboards/inventory/inventory.tsx | 28 +++++++------- .../dashboards/inventory/inventory_service.ts | 38 +++++++++++++++++-- .../dashboards/overview/dashboard.tsx | 10 ++--- ...ashboard-panels.ts => dashboard_panels.ts} | 0 ...filters.ts => dashboard_panels_filters.ts} | 0 ...anels-kpis.ts => dashboard_panels_kpis.ts} | 0 ...ss => vulnerability_detector_filters.scss} | 0 .../data_grid/use_data_grid.ts | 4 +- .../{searchbar => search_bar}/index.ts | 0 .../use_search_bar_configuration.test.ts} | 2 +- .../use_search_bar_configuration.tsx} | 0 13 files changed, 58 insertions(+), 26 deletions(-) rename plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/{use-dashboard-configuration.test.ts => use_dashboard_configuration.test.ts} (97%) rename plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/{use-dashboard-configuration.tsx => use_dashboard_configuration.tsx} (100%) rename plugins/main/public/components/overview/vulnerabilities/dashboards/overview/{dashboard-panels.ts => dashboard_panels.ts} (100%) rename plugins/main/public/components/overview/vulnerabilities/dashboards/overview/{dashboard-panels-filters.ts => dashboard_panels_filters.ts} (100%) rename plugins/main/public/components/overview/vulnerabilities/dashboards/overview/{dashboard-panels-kpis.ts => dashboard_panels_kpis.ts} (100%) rename plugins/main/public/components/overview/vulnerabilities/dashboards/overview/{vulnerability-detector-filters.scss => vulnerability_detector_filters.scss} (100%) rename plugins/main/public/components/overview/vulnerabilities/{searchbar => search_bar}/index.ts (100%) rename plugins/main/public/components/overview/vulnerabilities/{searchbar/use-search-bar-configuration.test.ts => search_bar/use_search_bar_configuration.test.ts} (99%) rename plugins/main/public/components/overview/vulnerabilities/{searchbar/use-search-bar-configuration.tsx => search_bar/use_search_bar_configuration.tsx} (100%) diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.test.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.test.ts similarity index 97% rename from plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.test.ts rename to plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.test.ts index f77c25e9b7..686277245e 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.test.ts +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.test.ts @@ -1,7 +1,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; import { DashboardContainerInput } from '../../../../../../../../src/plugins/dashboard/public'; -import { useDashboardConfiguration } from './use-dashboard-configuration'; +import { useDashboardConfiguration } from './use_dashboard_configuration'; describe('useDashboardConfiguration', () => { test('initial configuration is set correctly', () => { diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.tsx similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use-dashboard-configuration.tsx rename to plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.tsx diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx index 2b9634af9a..0e2e16fee2 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { getPlugins } from '../../../../../kibana-services'; -import useSearchBarConfiguration from '../../searchbar/use-search-bar-configuration' +import useSearchBarConfiguration from '../../search_bar/use_search_bar_configuration' import { IntlProvider } from 'react-intl'; import { EuiDataGrid, @@ -27,8 +27,10 @@ import { useDocViewer } from '../../doc_viewer/use_doc_viewer'; import './inventory.scss'; import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; import { search, exportSearchToCSV } from './inventory_service'; +import { ErrorHandler, ErrorFactory, HttpError } from '../../../../../react-services/error-management'; +import { withErrorBoundary } from '../../../../common/hocs'; -export const InventoryVuls = () => { +const InventoryVulsComponent = () => { const { searchBarProps } = useSearchBarConfiguration({ defaultIndexPatternID: VULNERABILITIES_INDEX_PATTERN_ID, }) @@ -39,7 +41,6 @@ export const InventoryVuls = () => { const [indexPattern, setIndexPattern] = useState(undefined); const [isSearching, setIsSearching] = useState(false); - const onClickInspectDoc = useMemo(() => (index: number) => { const rowClicked = results.hits.hits[index]; setInspectedHit(rowClicked); @@ -47,7 +48,6 @@ export const InventoryVuls = () => { const DocViewInspectButton = ({ rowIndex }: EuiDataGridCellValueElementProps) => { const inspectHintMsg = 'Inspect document details'; - return ( { setIsSearching(false); }); }catch(error){ - console.error(error); - // check when filters are wrong and the search fails + const searchError = ErrorFactory.create(HttpError, { error, message: 'Error searching vulnerabilities' }) + ErrorHandler.handleError(searchError); + setIsSearching(false); } } }, [JSON.stringify(searchBarProps), JSON.stringify(pagination), JSON.stringify(sorting)]); - - const timeField = indexPattern?.timeFieldName ? indexPattern.timeFieldName : undefined; const onClickExportResults = async () => { - // use the search method to get the data and pass the results.hits.total to make - const params = { indexPattern: indexPatterns?.[0] as IndexPattern, filters, @@ -113,11 +110,14 @@ export const InventoryVuls = () => { }, sorting } - - await exportSearchToCSV(params); + try { + await exportSearchToCSV(params); + }catch(error){ + const searchError = ErrorFactory.create(HttpError, { error, message: 'Error downloading csv report' }) + ErrorHandler.handleError(searchError); + } } - return ( { ); } + +export const InventoryVuls = withErrorBoundary(InventoryVulsComponent); \ No newline at end of file diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory_service.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory_service.ts index 645d9ff0dd..3aac10736e 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory_service.ts +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory_service.ts @@ -99,8 +99,40 @@ export const getFieldFormatted = (rowIndex, columnId, indexPattern, rowsParsed) } export const exportSearchToCSV = async (params: SearchParams): Promise => { - const { indexPattern, filters = [], query, sorting, fields } = params; - const searchResults = await search(params); + const DEFAULT_MAX_SIZE_PER_CALL = 10000; + const { indexPattern, filters = [], query, sorting, fields, pagination } = params; + // when the pageSize is greater than the default max size per call (10000) + // then we need to paginate the search + const mustPaginateSearch = pagination?.pageSize && pagination?.pageSize > DEFAULT_MAX_SIZE_PER_CALL; + const pageSize = mustPaginateSearch ? DEFAULT_MAX_SIZE_PER_CALL : pagination?.pageSize; + const totalHits = pagination?.pageSize || DEFAULT_MAX_SIZE_PER_CALL; + let pageIndex = params.pagination?.pageIndex || 0; + let hitsCount = 0; + let allHits = []; + let searchResults; + if (mustPaginateSearch) { + // paginate the search + while (hitsCount < totalHits) { + const searchParams = { + indexPattern, + filters, + query, + pagination: { + pageIndex, + pageSize, + }, + sorting, + fields, + }; + searchResults = await search(searchParams); + allHits = allHits.concat(searchResults.hits.hits); + hitsCount = allHits.length; + pageIndex++; + } + } else { + searchResults = await search(params); + } + const resultsFields = fields; const data = searchResults.hits.hits.map((hit) => { // check if the field type is a date @@ -137,8 +169,6 @@ export const exportSearchToCSV = async (params: SearchParams): Promise => return parsedRow?.join(','); }).join('\n'); - - // create a csv file using blob const blobData = new Blob( [ diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx index f028ddf770..e3810319c5 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { getPlugins } from '../../../../../kibana-services'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; -import { getDashboardPanels } from './dashboard-panels'; +import { getDashboardPanels } from './dashboard_panels'; import { I18nProvider } from '@osd/i18n/react'; -import useSearchBarConfiguration from '../../searchbar/use-search-bar-configuration'; +import useSearchBarConfiguration from '../../search_bar/use_search_bar_configuration'; import { VULNERABILITIES_INDEX_PATTERN_ID } from '../../common/constants'; -import { getDashboardFilters } from './dashboard-panels-filters'; -import './vulnerability-detector-filters.scss'; -import { getKPIsPanel } from './dashboard-panels-kpis'; +import { getDashboardFilters } from './dashboard_panels_filters'; +import './vulnerability_detector_filters.scss'; +import { getKPIsPanel } from './dashboard_panels_kpis'; const plugins = getPlugins(); const SearchBar = getPlugins().data.ui.SearchBar; diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard_panels.ts similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels.ts rename to plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard_panels.ts diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-filters.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard_panels_filters.ts similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-filters.ts rename to plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard_panels_filters.ts diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-kpis.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard_panels_kpis.ts similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard-panels-kpis.ts rename to plugins/main/public/components/overview/vulnerabilities/dashboards/overview/dashboard_panels_kpis.ts diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/vulnerability-detector-filters.scss b/plugins/main/public/components/overview/vulnerabilities/dashboards/overview/vulnerability_detector_filters.scss similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/dashboards/overview/vulnerability-detector-filters.scss rename to plugins/main/public/components/overview/vulnerabilities/dashboards/overview/vulnerability_detector_filters.scss diff --git a/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts b/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts index 4142d0c0f2..533ac5f7b4 100644 --- a/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts +++ b/plugins/main/public/components/overview/vulnerabilities/data_grid/use_data_grid.ts @@ -13,6 +13,8 @@ type tDataGridProps = { }; export const parseColumns = (fields: IFieldType[]): EuiDataGridColumn[] => { + // remove _source field becuase is a object field and is not supported + fields = fields.filter((field) => field.name !== '_source'); return fields.map((field) => { return { ...field, @@ -59,12 +61,10 @@ export const useDataGrid = (props: tDataGridProps): EuiDataGridProps => { setRows(results?.hits?.hits || []) }, [results, results?.hits, results?.hits?.total]) - useEffect(() => { setPagination((pagination) => ({ ...pagination, pageIndex: 0 })); }, [rowCount]) - const renderCellValue = ({ rowIndex, columnId, setCellProps }) => { const rowsParsed = parseData(rows); // On the context data always is stored the current page data (pagination) diff --git a/plugins/main/public/components/overview/vulnerabilities/searchbar/index.ts b/plugins/main/public/components/overview/vulnerabilities/search_bar/index.ts similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/searchbar/index.ts rename to plugins/main/public/components/overview/vulnerabilities/search_bar/index.ts diff --git a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.test.ts b/plugins/main/public/components/overview/vulnerabilities/search_bar/use_search_bar_configuration.test.ts similarity index 99% rename from plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.test.ts rename to plugins/main/public/components/overview/vulnerabilities/search_bar/use_search_bar_configuration.test.ts index 0de91e5bab..37685811d8 100644 --- a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.test.ts +++ b/plugins/main/public/components/overview/vulnerabilities/search_bar/use_search_bar_configuration.test.ts @@ -9,7 +9,7 @@ import { TimeRange, } from '../../../../../../../src/plugins/data/public'; // wazuh plugin dependencies -import useSearchBar from './use-search-bar-configuration'; +import useSearchBar from './use_search_bar_configuration'; import { getDataPlugin } from '../../../../kibana-services'; import * as timeFilterHook from '../../../common/hooks/use-time-filter'; import * as queryManagerHook from '../../../common/hooks/use-query'; diff --git a/plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx b/plugins/main/public/components/overview/vulnerabilities/search_bar/use_search_bar_configuration.tsx similarity index 100% rename from plugins/main/public/components/overview/vulnerabilities/searchbar/use-search-bar-configuration.tsx rename to plugins/main/public/components/overview/vulnerabilities/search_bar/use_search_bar_configuration.tsx From 09d3a84dcc29d9aa1d74d7d29538ff271d03f3f9 Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Thu, 2 Nov 2023 10:45:45 -0300 Subject: [PATCH 17/20] Remove unused hook --- .../hooks/use_dashboard_configuration.test.ts | 69 ------------------- .../hooks/use_dashboard_configuration.tsx | 33 --------- 2 files changed, 102 deletions(-) delete mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.test.ts delete mode 100644 plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.tsx diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.test.ts b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.test.ts deleted file mode 100644 index 686277245e..0000000000 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { renderHook, act } from '@testing-library/react-hooks'; -import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; -import { DashboardContainerInput } from '../../../../../../../../src/plugins/dashboard/public'; -import { useDashboardConfiguration } from './use_dashboard_configuration'; - -describe('useDashboardConfiguration', () => { - test('initial configuration is set correctly', () => { - const emptyInitialConfiguration: DashboardContainerInput = { - viewMode: ViewMode.EDIT, - filters: [], - query: undefined, - timeRange: undefined, - useMargins: false, - title: '', - isFullScreenMode: false, - panels: {}, - id: '', - }; - - const { result } = renderHook(() => useDashboardConfiguration()); - - expect(result.current.configuration).toEqual(emptyInitialConfiguration); - expect(typeof result.current.updateConfiguration).toBe('function'); - }); - - test('updateConfiguration updates configuration correctly', () => { - const { result } = renderHook(() => useDashboardConfiguration()); - - const updatedConfig = { - viewMode: ViewMode.VIEW, - title: 'Updated Title', - }; - - act(() => { - result.current.updateConfiguration(updatedConfig); - }); - - expect(result.current.configuration.viewMode).toBe(updatedConfig.viewMode); - expect(result.current.configuration.title).toBe(updatedConfig.title); - }); - - test('updateConfiguration merges properties correctly', () => { - const { result } = renderHook(() => useDashboardConfiguration()); - - const updatedConfig = { - viewMode: ViewMode.VIEW, - title: 'Updated Title', - }; - - act(() => { - result.current.updateConfiguration(updatedConfig); - }); - - expect(result.current.configuration.viewMode).toBe(updatedConfig.viewMode); - expect(result.current.configuration.title).toBe(updatedConfig.title); - - const additionalUpdate = { - description: 'Updated Description', - }; - - act(() => { - result.current.updateConfiguration(additionalUpdate); - }); - - expect(result.current.configuration.viewMode).toBe(updatedConfig.viewMode); - expect(result.current.configuration.title).toBe(updatedConfig.title); - expect(result.current.configuration.description).toBe(additionalUpdate.description); - }); -}); diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.tsx deleted file mode 100644 index bed04ebb19..0000000000 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/hooks/use_dashboard_configuration.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useState } from 'react'; -import { DashboardContainerInput } from '../../../../../../../../src/plugins/dashboard/public'; -import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; - -const emptyInitialConfiguration: DashboardContainerInput = { - viewMode: ViewMode.EDIT, - filters: [], - query: undefined, - timeRange: undefined, - useMargins: false, - title: '', - isFullScreenMode: false, - panels: {}, - id: '', -}; - -export const useDashboardConfiguration = (initialConfiguration?: DashboardContainerInput) => { - const [configuration, setConfiguration] = useState( - initialConfiguration ?? emptyInitialConfiguration - ); - - const updateConfiguration = (updatedConfig: Partial) => { - setConfiguration((prevConfig: DashboardContainerInput) => ({ - ...prevConfig, - ...updatedConfig, - })); - }; - - return { - configuration, - updateConfiguration, - }; -}; From 194e06574e253bc472363102ab94fe5a654a2e6b Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Thu, 2 Nov 2023 10:47:28 -0300 Subject: [PATCH 18/20] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb71211e4..99aa2812e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Wazuh 4.8.0 - Added remember server address check [#5791](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5791) - Added the ssl_agent_ca configuration to the SSL Settings form [#6083](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6083) +- Added global vulnerabilities dashboards [#5896](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5896) + ## Wazuh v4.7.1 - OpenSearch Dashboards 2.8.0 - Revision 00 From ec0129682f8b45d699b484183407be9361fad778 Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Thu, 2 Nov 2023 10:53:28 -0300 Subject: [PATCH 19/20] Fix CHANGELOG --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99aa2812e1..5e8349576e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Added the ssl_agent_ca configuration to the SSL Settings form [#6083](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6083) - Added global vulnerabilities dashboards [#5896](https://github.com/wazuh/wazuh-dashboard-plugins/pull/5896) - ## Wazuh v4.7.1 - OpenSearch Dashboards 2.8.0 - Revision 00 ### Added From 8cb221094cc7b75102b6ef1133b1ea644278aa39 Mon Sep 17 00:00:00 2001 From: Maximiliano Date: Thu, 2 Nov 2023 10:59:29 -0300 Subject: [PATCH 20/20] Remove explore agent in header --- .../main/public/components/common/modules/modules-defaults.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/main/public/components/common/modules/modules-defaults.js b/plugins/main/public/components/common/modules/modules-defaults.js index 75838b8d9b..517eeb84d6 100644 --- a/plugins/main/public/components/common/modules/modules-defaults.js +++ b/plugins/main/public/components/common/modules/modules-defaults.js @@ -136,13 +136,11 @@ export const ModulesDefaults = { { id: 'dashboard', name: 'Dashboard', - buttons: [ButtonModuleExploreAgent], component: withModuleNotForAgent(DashboardVuls), }, { id: 'inventory', name: 'Inventory', - buttons: [ButtonModuleExploreAgent], component: withModuleNotForAgent(InventoryVuls), }, EventsTab,