From b967d067c795288e14f1ee06667c2777c40b4887 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Tue, 1 Oct 2024 17:04:01 -0300 Subject: [PATCH 1/6] Scape doublequotes on export --- .../public/components/common/data-grid/data-grid-service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/main/public/components/common/data-grid/data-grid-service.ts b/plugins/main/public/components/common/data-grid/data-grid-service.ts index 35151a2f2d..cfceb1875a 100644 --- a/plugins/main/public/components/common/data-grid/data-grid-service.ts +++ b/plugins/main/public/components/common/data-grid/data-grid-service.ts @@ -170,7 +170,8 @@ export const exportSearchToCSV = async ( if (typeof value === 'object') { return JSON.stringify(value); } - return `"${value}"`; + // scape the double quotes and comma + return `"${value.toString().replaceAll(/"/g, '""')}"`; }); return parsedRow?.join(','); }) From 1c75e58cbf0e228d3ae8f9a907d10fa1d9e83ed7 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Wed, 2 Oct 2024 08:39:20 -0300 Subject: [PATCH 2/6] Add sanitize to the new line character --- .../public/components/common/data-grid/data-grid-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/main/public/components/common/data-grid/data-grid-service.ts b/plugins/main/public/components/common/data-grid/data-grid-service.ts index cfceb1875a..fd374b76b1 100644 --- a/plugins/main/public/components/common/data-grid/data-grid-service.ts +++ b/plugins/main/public/components/common/data-grid/data-grid-service.ts @@ -170,8 +170,8 @@ export const exportSearchToCSV = async ( if (typeof value === 'object') { return JSON.stringify(value); } - // scape the double quotes and comma - return `"${value.toString().replaceAll(/"/g, '""')}"`; + // Escape double quotes and handle line breaks to prevent column misalignment + return `"${value.toString().replaceAll(/"/g, '""').replaceAll(/\n/g, '\\n').replaceAll(/\r/g, '\\r')}"`; }); return parsedRow?.join(','); }) From 97f6e573a03bf129757e05645892f01e1f1d3693 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Wed, 2 Oct 2024 08:43:45 -0300 Subject: [PATCH 3/6] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c144e2ddeb..b46aeea8a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed read-only users could not access to Statistics application [#7001](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7001) - Fixed no-agent-alert spawn with selected agent in agent-welcome view [#7029](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7029) - Fixed security policy exception when it contained deprecated actions [#7042](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7042) +- Fixed export formatted csv data from tables with special characters [#7048](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7048) ### Removed From 297a15f5188a705f8c85de31340be6cc2cc44942 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Wed, 2 Oct 2024 08:45:58 -0300 Subject: [PATCH 4/6] Apply prettier --- .../common/data-grid/data-grid-service.ts | 85 +++++++------------ 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/plugins/main/public/components/common/data-grid/data-grid-service.ts b/plugins/main/public/components/common/data-grid/data-grid-service.ts index fd374b76b1..620d40aaca 100644 --- a/plugins/main/public/components/common/data-grid/data-grid-service.ts +++ b/plugins/main/public/components/common/data-grid/data-grid-service.ts @@ -2,11 +2,7 @@ import { SearchResponse } from '../../../../../../src/core/server'; import * as FileSaver from '../../../services/file-saver'; import { beautifyDate } from '../../agents/vuls/inventory/lib'; import { SearchParams, search } from '../search-bar/search-bar-service'; -import { - Filter, - IFieldType, - IndexPattern, -} from '../../../../../../src/plugins/data/common'; +import { Filter, IFieldType, IndexPattern } from '../../../../../../src/plugins/data/common'; export const MAX_ENTRIES_PER_QUERY = 10000; import { tDataGridColumn } from './use-data-grid'; import { cellFilterActions } from './cell-filter-actions'; @@ -26,9 +22,9 @@ type ParseData = | {}; export const parseData = ( - resultsHits: SearchResponse['hits']['hits'], + resultsHits: SearchResponse['hits']['hits'] ): ParseData[] => { - const data = resultsHits.map(hit => { + const data = resultsHits.map((hit) => { if (!hit) { return {}; } @@ -49,16 +45,16 @@ export const getFieldFormatted = ( rowIndex: number, columnId: string, indexPattern: IndexPattern, - rowsParsed: ParseData[], + rowsParsed: ParseData[] ) => { - const field = indexPattern.fields.find(field => field.name === columnId); + 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 => { + nestedFields.forEach((field) => { if (fieldValue) { fieldValue = fieldValue[field]; } @@ -91,25 +87,14 @@ export const getFieldFormatted = ( }; // receive search params -export const exportSearchToCSV = async ( - params: SearchParams, -): Promise => { +export const exportSearchToCSV = async (params: SearchParams): Promise => { const DEFAULT_MAX_SIZE_PER_CALL = 1000; - const { - indexPattern, - filters = [], - query, - sorting, - fields, - pagination, - } = params; + 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 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; @@ -140,13 +125,13 @@ export const exportSearchToCSV = async ( } const resultsFields = fields; - const data = allHits.map(hit => { + const data = allHits.map((hit) => { // check if the field type is a date const dateFields = indexPattern.fields.getByType('date'); - const dateFieldsNames = dateFields.map(field => field.name); + const dateFieldsNames = dateFields.map((field) => field.name); const flattenHit = indexPattern.flattenHit(hit); // replace the date fields with the formatted date - dateFieldsNames.forEach(field => { + dateFieldsNames.forEach((field) => { if (flattenHit[field]) { flattenHit[field] = beautifyDate(flattenHit[field]); } @@ -161,8 +146,8 @@ export const exportSearchToCSV = async ( if (!data || data.length === 0) return; const parsedData = data - .map(row => { - const parsedRow = resultsFields?.map(field => { + .map((row) => { + const parsedRow = resultsFields?.map((field) => { const value = row[field]; if (value === undefined || value === null) { return ''; @@ -171,7 +156,11 @@ export const exportSearchToCSV = async ( return JSON.stringify(value); } // Escape double quotes and handle line breaks to prevent column misalignment - return `"${value.toString().replaceAll(/"/g, '""').replaceAll(/\n/g, '\\n').replaceAll(/\r/g, '\\r')}"`; + return `"${value + .toString() + .replaceAll(/"/g, '""') + .replaceAll(/\n/g, '\\n') + .replaceAll(/\r/g, '\\r')}"`; }); return parsedRow?.join(','); }) @@ -191,18 +180,18 @@ export const exportSearchToCSV = async ( const onFilterCellActions = ( indexPatternId: string, filters: Filter[], - setFilters: (filters: Filter[]) => void, + setFilters: (filters: Filter[]) => void ) => { return ( columndId: string, value: any, - operation: FILTER_OPERATOR.IS | FILTER_OPERATOR.IS_NOT, + operation: FILTER_OPERATOR.IS | FILTER_OPERATOR.IS_NOT ) => { const newFilter = PatternDataSourceFilterManager.createFilter( operation, columndId, value, - indexPatternId, + indexPatternId ); setFilters([...filters, newFilter]); }; @@ -215,9 +204,9 @@ const mapToDataGridColumn = ( pageSize: number, filters: Filter[], setFilters: (filters: Filter[]) => void, - defaultColumns: tDataGridColumn[], + defaultColumns: tDataGridColumn[] ): tDataGridColumn => { - const defaultColumn = defaultColumns.find(column => column.id === field.name); + const defaultColumn = defaultColumns.find((column) => column.id === field.name); return { ...field, id: field.name, @@ -230,7 +219,7 @@ const mapToDataGridColumn = ( indexPattern, rows, pageSize, - onFilterCellActions(indexPattern.id as string, filters, setFilters), + onFilterCellActions(indexPattern.id as string, filters, setFilters) ), } as tDataGridColumn; }; @@ -242,24 +231,16 @@ export const parseColumns = ( rows: any[], pageSize: number, filters: Filter[], - setFilters: (filters: Filter[]) => void, + setFilters: (filters: Filter[]) => void ): tDataGridColumn[] => { // remove _source field becuase is a object field and is not supported // merge the properties of the field with the default columns if (!fields?.length) return defaultColumns; return fields - .filter(field => field.name !== '_source') - .map(field => - mapToDataGridColumn( - field, - indexPattern, - rows, - pageSize, - filters, - setFilters, - defaultColumns, - ), + .filter((field) => field.name !== '_source') + .map((field) => + mapToDataGridColumn(field, indexPattern, rows, pageSize, filters, setFilters, defaultColumns) ); }; @@ -272,12 +253,12 @@ export const parseColumns = ( */ export const getAllCustomRenders = ( customColumns: tDataGridColumn[], - discoverColumns: tDataGridColumn[], + discoverColumns: tDataGridColumn[] ): tDataGridColumn[] => { - const customColumnsWithRender = customColumns.filter(column => column.render); - const allColumns = discoverColumns.map(column => { + const customColumnsWithRender = customColumns.filter((column) => column.render); + const allColumns = discoverColumns.map((column) => { const customColumn = customColumnsWithRender.find( - customColumn => customColumn.id === column.id, + (customColumn) => customColumn.id === column.id ); return customColumn || column; }); From b0524d01788f92c1d26fea761085599752c6455b Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Wed, 2 Oct 2024 09:01:31 -0300 Subject: [PATCH 5/6] Apply prettier --- CHANGELOG.md | 2 +- .../common/data-grid/data-grid-service.ts | 79 ++++++++++++------- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a23b8de5..812a33f3e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed read-only users could not access to Statistics application [#7001](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7001) - Fixed no-agent-alert spawn with selected agent in agent-welcome view [#7029](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7029) - Fixed security policy exception when it contained deprecated actions [#7042](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7042) -- Fixed export formatted csv data from tables with special characters [#7048](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7048) +- Fix export formatted csv data with special characters from tables [#7048](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7048) ### Removed diff --git a/plugins/main/public/components/common/data-grid/data-grid-service.ts b/plugins/main/public/components/common/data-grid/data-grid-service.ts index 620d40aaca..97d66428a8 100644 --- a/plugins/main/public/components/common/data-grid/data-grid-service.ts +++ b/plugins/main/public/components/common/data-grid/data-grid-service.ts @@ -2,7 +2,11 @@ import { SearchResponse } from '../../../../../../src/core/server'; import * as FileSaver from '../../../services/file-saver'; import { beautifyDate } from '../../agents/vuls/inventory/lib'; import { SearchParams, search } from '../search-bar/search-bar-service'; -import { Filter, IFieldType, IndexPattern } from '../../../../../../src/plugins/data/common'; +import { + Filter, + IFieldType, + IndexPattern, +} from '../../../../../../src/plugins/data/common'; export const MAX_ENTRIES_PER_QUERY = 10000; import { tDataGridColumn } from './use-data-grid'; import { cellFilterActions } from './cell-filter-actions'; @@ -22,9 +26,9 @@ type ParseData = | {}; export const parseData = ( - resultsHits: SearchResponse['hits']['hits'] + resultsHits: SearchResponse['hits']['hits'], ): ParseData[] => { - const data = resultsHits.map((hit) => { + const data = resultsHits.map(hit => { if (!hit) { return {}; } @@ -45,16 +49,16 @@ export const getFieldFormatted = ( rowIndex: number, columnId: string, indexPattern: IndexPattern, - rowsParsed: ParseData[] + rowsParsed: ParseData[], ) => { - const field = indexPattern.fields.find((field) => field.name === columnId); + 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) => { + nestedFields.forEach(field => { if (fieldValue) { fieldValue = fieldValue[field]; } @@ -87,14 +91,25 @@ export const getFieldFormatted = ( }; // receive search params -export const exportSearchToCSV = async (params: SearchParams): Promise => { +export const exportSearchToCSV = async ( + params: SearchParams, +): Promise => { const DEFAULT_MAX_SIZE_PER_CALL = 1000; - const { indexPattern, filters = [], query, sorting, fields, pagination } = params; + 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 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; @@ -125,13 +140,13 @@ export const exportSearchToCSV = async (params: SearchParams): Promise => } const resultsFields = fields; - const data = allHits.map((hit) => { + const data = allHits.map(hit => { // check if the field type is a date const dateFields = indexPattern.fields.getByType('date'); - const dateFieldsNames = dateFields.map((field) => field.name); + const dateFieldsNames = dateFields.map(field => field.name); const flattenHit = indexPattern.flattenHit(hit); // replace the date fields with the formatted date - dateFieldsNames.forEach((field) => { + dateFieldsNames.forEach(field => { if (flattenHit[field]) { flattenHit[field] = beautifyDate(flattenHit[field]); } @@ -146,8 +161,8 @@ export const exportSearchToCSV = async (params: SearchParams): Promise => if (!data || data.length === 0) return; const parsedData = data - .map((row) => { - const parsedRow = resultsFields?.map((field) => { + .map(row => { + const parsedRow = resultsFields?.map(field => { const value = row[field]; if (value === undefined || value === null) { return ''; @@ -180,18 +195,18 @@ export const exportSearchToCSV = async (params: SearchParams): Promise => const onFilterCellActions = ( indexPatternId: string, filters: Filter[], - setFilters: (filters: Filter[]) => void + setFilters: (filters: Filter[]) => void, ) => { return ( columndId: string, value: any, - operation: FILTER_OPERATOR.IS | FILTER_OPERATOR.IS_NOT + operation: FILTER_OPERATOR.IS | FILTER_OPERATOR.IS_NOT, ) => { const newFilter = PatternDataSourceFilterManager.createFilter( operation, columndId, value, - indexPatternId + indexPatternId, ); setFilters([...filters, newFilter]); }; @@ -204,9 +219,9 @@ const mapToDataGridColumn = ( pageSize: number, filters: Filter[], setFilters: (filters: Filter[]) => void, - defaultColumns: tDataGridColumn[] + defaultColumns: tDataGridColumn[], ): tDataGridColumn => { - const defaultColumn = defaultColumns.find((column) => column.id === field.name); + const defaultColumn = defaultColumns.find(column => column.id === field.name); return { ...field, id: field.name, @@ -219,7 +234,7 @@ const mapToDataGridColumn = ( indexPattern, rows, pageSize, - onFilterCellActions(indexPattern.id as string, filters, setFilters) + onFilterCellActions(indexPattern.id as string, filters, setFilters), ), } as tDataGridColumn; }; @@ -231,16 +246,24 @@ export const parseColumns = ( rows: any[], pageSize: number, filters: Filter[], - setFilters: (filters: Filter[]) => void + setFilters: (filters: Filter[]) => void, ): tDataGridColumn[] => { // remove _source field becuase is a object field and is not supported // merge the properties of the field with the default columns if (!fields?.length) return defaultColumns; return fields - .filter((field) => field.name !== '_source') - .map((field) => - mapToDataGridColumn(field, indexPattern, rows, pageSize, filters, setFilters, defaultColumns) + .filter(field => field.name !== '_source') + .map(field => + mapToDataGridColumn( + field, + indexPattern, + rows, + pageSize, + filters, + setFilters, + defaultColumns, + ), ); }; @@ -253,12 +276,12 @@ export const parseColumns = ( */ export const getAllCustomRenders = ( customColumns: tDataGridColumn[], - discoverColumns: tDataGridColumn[] + discoverColumns: tDataGridColumn[], ): tDataGridColumn[] => { - const customColumnsWithRender = customColumns.filter((column) => column.render); - const allColumns = discoverColumns.map((column) => { + const customColumnsWithRender = customColumns.filter(column => column.render); + const allColumns = discoverColumns.map(column => { const customColumn = customColumnsWithRender.find( - (customColumn) => customColumn.id === column.id + customColumn => customColumn.id === column.id, ); return customColumn || column; }); From 5b127f1d9193d30605c2309e2cc1392aa327f84b Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Wed, 2 Oct 2024 13:10:11 -0300 Subject: [PATCH 6/6] Scape linebreak --- .../public/components/common/data-grid/data-grid-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/main/public/components/common/data-grid/data-grid-service.ts b/plugins/main/public/components/common/data-grid/data-grid-service.ts index 97d66428a8..bdd6cace7e 100644 --- a/plugins/main/public/components/common/data-grid/data-grid-service.ts +++ b/plugins/main/public/components/common/data-grid/data-grid-service.ts @@ -174,8 +174,8 @@ export const exportSearchToCSV = async ( return `"${value .toString() .replaceAll(/"/g, '""') - .replaceAll(/\n/g, '\\n') - .replaceAll(/\r/g, '\\r')}"`; + .replaceAll(/\r\n/g, '\\r\\n') + .replaceAll(/\n/g, '\\n')}"`; }); return parsedRow?.join(','); })