diff --git a/CHANGELOG.md b/CHANGELOG.md index 51dfa62e0e..6d8d7a74ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added - Support for Wazuh 4.10.2 +- Add setting to limit the number of rows in csv reports [#7182](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7182) ### Changed diff --git a/plugins/main/public/components/agents/syscollector/inventory.test.tsx b/plugins/main/public/components/agents/syscollector/inventory.test.tsx index b39238c7cc..95a08ff30e 100644 --- a/plugins/main/public/components/agents/syscollector/inventory.test.tsx +++ b/plugins/main/public/components/agents/syscollector/inventory.test.tsx @@ -4,6 +4,16 @@ import { SyscollectorInventory } from './inventory'; import { AgentTabs } from '../../endpoints-summary/agent/agent-tabs'; import { queryDataTestAttr } from '../../../../test/public/query-attr'; +jest.mock('../../common/hooks/use-app-config', () => ({ + useAppConfig: () => ({ + isReady: true, + isLoading: false, + data: { + 'reports.csv.maxRows': 10000, + }, + }), +})); + const TABLE_ID = '__table_7d62db31-1cd0-11ee-8e0c-33242698a3b9'; const SOFTWARE_PACKAGES = 'Packages'; const SOFTWARE_WINDOWS_UPDATES = 'Windows updates'; diff --git a/plugins/main/public/components/common/tables/components/export-table-csv.tsx b/plugins/main/public/components/common/tables/components/export-table-csv.tsx index d4bc30b2dc..1ec3e500f5 100644 --- a/plugins/main/public/components/common/tables/components/export-table-csv.tsx +++ b/plugins/main/public/components/common/tables/components/export-table-csv.tsx @@ -11,18 +11,20 @@ */ import React from 'react'; -import { - EuiFlexItem, - EuiButtonEmpty -} from '@elastic/eui'; +import { EuiFlexItem, EuiButtonEmpty, EuiIconTip } from '@elastic/eui'; import exportCsv from '../../../../react-services/wz-csv'; -import { getToasts } from '../../../../kibana-services'; +import { getToasts } from '../../../../kibana-services'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; -export function ExportTableCsv({ endpoint, totalItems, filters, title }) { - +export function ExportTableCsv({ + endpoint, + totalItems, + filters, + title, + maxRows, +}) { const showToast = (color, title, time) => { getToasts().add({ color: color, @@ -33,15 +35,12 @@ export function ExportTableCsv({ endpoint, totalItems, filters, title }) { const downloadCsv = async () => { try { - const formatedFilters = Object.entries(filters).map(([name, value]) => ({name, value})); + const formatedFilters = Object.entries(filters).map(([name, value]) => ({ + name, + value, + })); showToast('success', 'Your download should begin automatically...', 3000); - await exportCsv( - endpoint, - [ - ...formatedFilters - ], - `${(title).toLowerCase()}` - ); + await exportCsv(endpoint, [...formatedFilters], `${title.toLowerCase()}`); } catch (error) { const options = { context: `${ExportTableCsv.name}.downloadCsv`, @@ -55,19 +54,36 @@ export function ExportTableCsv({ endpoint, totalItems, filters, title }) { }; getErrorOrchestrator().handleError(options); } - } - - return - downloadCsv()}> - Export formatted - + }; + + return ( + + downloadCsv()} + > + Export formatted + {totalItems > maxRows && ( + <> + {' '} + App Settings`} + size='m' + color='primary' + type='iInCircle' + /> + + )} + + ); } // Set default props ExportTableCsv.defaultProps = { - endpoint:'/', - totalItems:0, - filters: [], - title:"" - }; \ No newline at end of file + endpoint: '/', + totalItems: 0, + filters: [], + title: '', +}; diff --git a/plugins/main/public/components/common/tables/table-wz-api.test.tsx b/plugins/main/public/components/common/tables/table-wz-api.test.tsx index b6b98e5dde..00715188d1 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.test.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.test.tsx @@ -15,6 +15,12 @@ import React from 'react'; import { mount } from 'enzyme'; import { TableWzAPI } from './table-wz-api'; +import { useAppConfig, useStateStorage } from '../hooks'; + +jest.mock('../hooks', () => ({ + useAppConfig: jest.fn(), + useStateStorage: jest.fn(), +})); jest.mock('../../../kibana-services', () => ({ getHttp: () => ({ @@ -64,6 +70,13 @@ const columns = [ describe('Table WZ API component', () => { it('renders correctly to match the snapshot', () => { + (useAppConfig as jest.Mock).mockReturnValue({ + data: { + 'reports.csv.maxRows': 10000, + }, + }); + (useStateStorage as jest.Mock).mockReturnValue([[], jest.fn()]); + const wrapper = mount( { +interface Filters { + [key: string]: string; +} + +const getFilters = (filters: Filters) => { const { default: defaultFilters, ...restFilters } = filters; return Object.keys(restFilters).length ? restFilters : defaultFilters; }; +const formatSorting = sorting => { + if (!sorting.field || !sorting.direction) { + return ''; + } + return `${sorting.direction === 'asc' ? '+' : '-'}${sorting.field}`; +}; + export function TableWzAPI({ actionButtons, postActionButtons, @@ -53,12 +63,11 @@ export function TableWzAPI({ actionButtons?: | ReactNode | ReactNode[] - | (({ filters }: { filters }) => ReactNode); + | (({ filters }: { filters: Filters }) => ReactNode); postActionButtons?: | ReactNode | ReactNode[] - | (({ filters }: { filters }) => ReactNode); - + | (({ filters }: { filters: Filters }) => ReactNode); title?: string; addOnTitle?: ReactNode; description?: string; @@ -67,7 +76,7 @@ export function TableWzAPI({ searchTable?: boolean; endpoint: string; buttonOptions?: CustomFilterButton[]; - onFiltersChange?: Function; + onFiltersChange?: (filters: Filters) => void; showReload?: boolean; searchBarProps?: any; reload?: boolean; @@ -75,10 +84,10 @@ export function TableWzAPI({ setReload?: (newValue: number) => void; }) { const [totalItems, setTotalItems] = useState(0); - const [filters, setFilters] = useState({}); + const [filters, setFilters] = useState({}); const [isLoading, setIsLoading] = useState(false); - - const onFiltersChange = filters => + const [sort, setSort] = useState({}); + const onFiltersChange = (filters: Filters) => typeof rest.onFiltersChange === 'function' ? rest.onFiltersChange(filters) : null; @@ -101,16 +110,17 @@ export function TableWzAPI({ : undefined, ); const [isOpenFieldSelector, setIsOpenFieldSelector] = useState(false); - + const appConfig = useAppConfig(); + const maxRows = appConfig.data['reports.csv.maxRows']; const onSearch = useCallback(async function ( endpoint, - filters, + filters: Filters, pagination, sorting, ) { try { const { pageIndex, pageSize } = pagination; - const { field, direction } = sorting.sort; + setSort(sorting.sort); setIsLoading(true); setFilters(filters); onFiltersChange(filters); @@ -118,9 +128,8 @@ export function TableWzAPI({ ...getFilters(filters), offset: pageIndex * pageSize, limit: pageSize, - sort: `${direction === 'asc' ? '+' : '-'}${field}`, + sort: formatSorting(sorting.sort), }; - const response = await WzRequest.apiReq('GET', endpoint, { params }); const { affected_items: items, total_affected_items: totalItems } = ( @@ -182,7 +191,9 @@ export function TableWzAPI({ }; useEffect(() => { - if (rest.reload) triggerReload(); + if (rest.reload) { + triggerReload(); + } }, [rest.reload]); const ReloadButton = ( @@ -227,16 +238,22 @@ export function TableWzAPI({ {rest.showReload && ReloadButton} {/* Render optional export to CSV button */} {rest.downloadCsv && ( - + <> + + )} {/* Render optional post custom action button */} {renderActionButtons(postActionButtons, filters)} diff --git a/plugins/main/public/components/endpoints-summary/table/agents-table.test.tsx b/plugins/main/public/components/endpoints-summary/table/agents-table.test.tsx index fb164371c4..fa97f085b2 100644 --- a/plugins/main/public/components/endpoints-summary/table/agents-table.test.tsx +++ b/plugins/main/public/components/endpoints-summary/table/agents-table.test.tsx @@ -5,6 +5,16 @@ import { WzRequest } from '../../../react-services/wz-request'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; +jest.mock('../../common/hooks/use-app-config', () => ({ + useAppConfig: () => ({ + isReady: true, + isLoading: false, + data: { + 'reports.csv.maxRows': 10000, + }, + }), +})); + const data = [ { id: '001', diff --git a/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx b/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx index 2f18e4262d..cabb0900ca 100644 --- a/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx +++ b/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx @@ -18,6 +18,15 @@ import { ModuleMitreAttackIntelligence } from './intelligence'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; +jest.mock('../../../common/hooks/use-app-config', () => ({ + useAppConfig: () => ({ + isReady: true, + isLoading: false, + data: { + 'reports.csv.maxRows': 10000, + }, + }), +})); jest.mock( '../../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index 545e989e85..f215852177 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -734,6 +734,8 @@ export class WazuhApiCtrl { request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory, ) { + const appConfig = await context.wazuh_core.configuration.get(); + const reportMaxRows = appConfig['reports.csv.maxRows']; try { if (!request.body || !request.body.path) throw new Error('Field path is required'); @@ -782,16 +784,25 @@ export class WazuhApiCtrl { if (totalItems && !isList) { params.offset = 0; - itemsArray.push(...output.data.data.affected_items); - while (itemsArray.length < totalItems && params.offset < totalItems) { - params.offset += params.limit; + while ( + itemsArray.length < Math.min(totalItems, reportMaxRows) && + params.offset < Math.min(totalItems, reportMaxRows) + ) { const tmpData = await context.wazuh.api.client.asCurrentUser.request( 'GET', `/${tmpPath}`, { params: params }, { apiHostID: request.body.id }, ); - itemsArray.push(...tmpData.data.data.affected_items); + + const affectedItems = tmpData.data.data.affected_items; + const remainingItems = reportMaxRows - itemsArray.length; + if (itemsArray.length + affectedItems.length > reportMaxRows) { + itemsArray.push(...affectedItems.slice(0, remainingItems)); + break; + } + itemsArray.push(...affectedItems); + params.offset += params.limit; } } diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 3b51f2e9bf..c9e10f7941 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -1845,6 +1845,41 @@ hosts: return SettingsValidator.number(this.options.number)(value); }, }, + 'reports.csv.maxRows': { + title: 'Max rows in csv reports', + store: { + file: { + configurableManaged: true, + }, + }, + description: + 'Maximum rows that will be included in csv reports. If the number of rows exceeds this value, the report will be truncated. Setting a high value can cause instability of the report generating process.', + category: SettingCategory.GENERAL, + type: EpluginSettingType.number, + defaultValue: 10000, + isConfigurableFromSettings: true, + options: { + number: { + integer: true, + }, + }, + uiFormTransformConfigurationValueToInputValue: function (value: number) { + return String(value); + }, + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { + return Number(value); + }, + validateUIForm: function (value) { + return this.validate( + this.uiFormTransformInputValueToConfigurationValue(value), + ); + }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + }, 'wazuh.monitoring.creation': { title: 'Index creation', description: