From 2ec2c930de6d0f7c195b429ab5c33224074f1cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 7 Mar 2024 16:22:42 +0100 Subject: [PATCH 01/12] feat(reporting): generation of PDF reports downloads the file to the client instead of storing into the filesystem of the backend - Generation of PDF reports downloads the file to the client instead of storing into the filesystem of the backend - Refactor the Printer.print service to use a Buffer - Create a frontend service to save the file from the API response - Refactor the ReportingService to use the new generic service to save the file from the response - Adapt the endpoints that generate reports to return the file - POST /reports/modules/{moduleID} - POST /reports/groups/{groupID} - POST /reports/agents/{agentID} - POST /reports/agents/{agentID}/inventory --- .../main/public/react-services/export-file.ts | 33 +++++++++++++++ .../main/public/react-services/reporting.js | 27 +++++-------- .../main/public/react-services/wz-request.ts | 7 ++++ .../server/controllers/wazuh-reporting.ts | 40 +++++++++++-------- plugins/main/server/lib/reporting/printer.ts | 15 +++++-- 5 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 plugins/main/public/react-services/export-file.ts diff --git a/plugins/main/public/react-services/export-file.ts b/plugins/main/public/react-services/export-file.ts new file mode 100644 index 0000000000..9ec563c30b --- /dev/null +++ b/plugins/main/public/react-services/export-file.ts @@ -0,0 +1,33 @@ +import { getToasts } from '../kibana-services'; +import * as FileSaver from '../services/file-saver'; + +interface IExportResponseToFile { + headers: { + 'content-disposition'?: string; + 'content-type': string; + }; +} +/** + * Export response data to file + */ +export const exportResponseToFile = async (response: IExportResponseToFile) => { + // Get the filename from the response headers + const [_, filename] = + response?.headers?.['content-disposition']?.match?.(/filename="([^"]+)"/) || + []; + + // Create blob + const blob = new Blob([response.data], { + type: response.headers['content-type'], + }); + + // Save file from frontend side + FileSaver.saveAs(blob, filename); + + // Display a toast message + getToasts().add({ + color: 'success', + title: 'File downloaded', + toastLifeTimeMs: 4000, + }); +}; diff --git a/plugins/main/public/react-services/reporting.js b/plugins/main/public/react-services/reporting.js index 2306b60d80..8f1612a5e8 100644 --- a/plugins/main/public/react-services/reporting.js +++ b/plugins/main/public/react-services/reporting.js @@ -22,6 +22,8 @@ import { getAngularModule, getDataPlugin, getToasts } from '../kibana-services'; import { UI_LOGGER_LEVELS } from '../../common/constants'; import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; import { getErrorOrchestrator } from './common-services'; +import { exportResponseToFile } from './export-file'; + const app = getAngularModule(); export class ReportingService { @@ -123,18 +125,15 @@ export class ReportingService { tab === 'syscollector' ? `/reports/agents/${agents}/inventory` : `/reports/modules/${tab}`; - await WzRequest.genericReq('POST', apiEndpoint, data); + const response = await WzRequest.genericReq('POST', apiEndpoint, data, { + requestClientOptions: { responseType: 'blob' }, + }); + + exportResponseToFile(response); this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; this.$rootScope.$applyAsync(); - this.showToast( - 'success', - 'Created report', - 'Success. Go to Dashboard management > Reporting', - 4000, - ); - return; } catch (error) { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; @@ -174,18 +173,14 @@ export class ReportingService { type === 'agentConfig' ? `/reports/agents/${obj.id}` : `/reports/groups/${obj.name}`; - await WzRequest.genericReq('POST', apiEndpoint, data); + const response = await WzRequest.genericReq('POST', apiEndpoint, data, { + requestClientOptions: { responseType: 'blob' }, + }); + exportResponseToFile(response); this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; this.$rootScope.$applyAsync(); - this.showToast( - 'success', - 'Created report', - 'Success. Go to Dashboard management > Reporting', - 4000, - ); - return; } catch (error) { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; diff --git a/plugins/main/public/react-services/wz-request.ts b/plugins/main/public/react-services/wz-request.ts index fa0785eb29..a55ad7b2c6 100644 --- a/plugins/main/public/react-services/wz-request.ts +++ b/plugins/main/public/react-services/wz-request.ts @@ -36,10 +36,12 @@ export class WzRequest { shouldRetry?: boolean; checkCurrentApiIsUp?: boolean; overwriteHeaders?: any; + requestClientOptions?: any; } = { shouldRetry: true, checkCurrentApiIsUp: true, overwriteHeaders: {}, + requestClientOptions: {}, }, ) { const shouldRetry = @@ -54,6 +56,10 @@ export class WzRequest { typeof extraOptions.overwriteHeaders === 'object' ? extraOptions.overwriteHeaders : {}; + const requestClientOptions = + typeof extraOptions.requestClientOptions === 'object' + ? extraOptions.requestClientOptions + : {}; try { if (!method || !path) { throw new Error('Missing parameters'); @@ -73,6 +79,7 @@ export class WzRequest { url: url, data: payload, timeout: timeout, + ...requestClientOptions, }; const data = await request(options); diff --git a/plugins/main/server/controllers/wazuh-reporting.ts b/plugins/main/server/controllers/wazuh-reporting.ts index 5c00d692b0..b046a4c515 100644 --- a/plugins/main/server/controllers/wazuh-reporting.ts +++ b/plugins/main/server/controllers/wazuh-reporting.ts @@ -385,13 +385,15 @@ export class WazuhReportingCtrl { printer.addAgentsFilters(agentsFilter.agentsText); } - await printer.print(context.wazuhEndpointParams.pathFilename); + const buffer = await printer.print(); return response.ok({ - body: { - success: true, - message: `Report ${context.wazuhEndpointParams.filename} was created`, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment;filename="${context.wazuhEndpointParams.filename}"`, + 'Cache-Control': 'no-store,no-cache', }, + body: buffer, }); } catch (error) { return ErrorResponse(error.message || error, 5029, 500, response); @@ -678,13 +680,15 @@ export class WazuhReportingCtrl { ); } - await printer.print(context.wazuhEndpointParams.pathFilename); + const buffer = await printer.print(); return response.ok({ - body: { - success: true, - message: `Report ${context.wazuhEndpointParams.filename} was created`, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment;filename="${context.wazuhEndpointParams.filename}"`, + 'Cache-Control': 'no-store,no-cache', }, + body: buffer, }); } catch (error) { context.wazuh.logger.error(error.message || error); @@ -1016,13 +1020,15 @@ export class WazuhReportingCtrl { } } - await printer.print(context.wazuhEndpointParams.pathFilename); + const buffer = await printer.print(); return response.ok({ - body: { - success: true, - message: `Report ${context.wazuhEndpointParams.filename} was created`, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment;filename="${context.wazuhEndpointParams.filename}"`, + 'Cache-Control': 'no-store,no-cache', }, + body: buffer, }); } catch (error) { context.wazuh.logger.debug(error.message || error); @@ -1305,13 +1311,15 @@ export class WazuhReportingCtrl { .forEach(table => printer.addSimpleTable(table)); // Print the document - await printer.print(context.wazuhEndpointParams.pathFilename); + const buffer = await printer.print(); return response.ok({ - body: { - success: true, - message: `Report ${context.wazuhEndpointParams.filename} was created`, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment;filename="${context.wazuhEndpointParams.filename}"`, + 'Cache-Control': 'no-store,no-cache', }, + body: buffer, }); } catch (error) { context.wazuh.logger.error(error.message || error); diff --git a/plugins/main/server/lib/reporting/printer.ts b/plugins/main/server/lib/reporting/printer.ts index 091a3cb082..13038a5a62 100644 --- a/plugins/main/server/lib/reporting/printer.ts +++ b/plugins/main/server/lib/reporting/printer.ts @@ -1,6 +1,6 @@ -import fs from 'fs'; import path from 'path'; import PdfPrinter from 'pdfmake/src/printer'; +import { Writable } from 'stream'; import clockIconRaw from './clock-icon-raw'; import filterIconRaw from './filter-icon-raw'; import { @@ -602,7 +602,7 @@ export class ReportPrinter { this.logger.debug('Time range and filters rendered'); } - async print(reportPath: string) { + async print() { return new Promise((resolve, reject) => { try { const configuration = getConfiguration(); @@ -620,15 +620,22 @@ export class ReportPrinter { 'customization.reports.footer', ); + const buffers = []; + const pdfWritableStream = new Writable({ + write(chunk, encoding, callback) { + buffers.push(chunk); + callback(); + }, + }); const document = this._printer.createPdfKitDocument({ ...pageConfiguration({ pathToLogo, pageHeader, pageFooter }), content: this._content, }); document.on('error', reject); - document.on('end', resolve); + document.on('end', () => resolve(Buffer.concat(buffers))); - document.pipe(fs.createWriteStream(reportPath)); + document.pipe(pdfWritableStream); document.end(); } catch (ex) { reject(ex); From 5e9ed5243a49aee47f58d34a4e6b40123c84d5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 7 Mar 2024 16:59:37 +0100 Subject: [PATCH 02/12] remove(reporting): remove Reporting application - Remove reporting application - Remove render components files --- .../components/management/management-main.js | 2 - .../reporting-main.test.tsx.snap | 7 - .../management/reporting/reporting-main.js | 44 ---- .../reporting/reporting-main.test.tsx | 33 --- .../reporting/reporting-overview.js | 78 ------- .../management/reporting/reporting-table.js | 193 ------------------ .../reporting/utils/actions-buttons-main.js | 97 --------- .../reporting/utils/columns-main.js | 100 --------- .../reporting/utils/reporting-handler.js | 44 ---- plugins/main/public/utils/applications.ts | 20 -- 10 files changed, 618 deletions(-) delete mode 100644 plugins/main/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap delete mode 100644 plugins/main/public/controllers/management/components/management/reporting/reporting-main.js delete mode 100644 plugins/main/public/controllers/management/components/management/reporting/reporting-main.test.tsx delete mode 100644 plugins/main/public/controllers/management/components/management/reporting/reporting-overview.js delete mode 100644 plugins/main/public/controllers/management/components/management/reporting/reporting-table.js delete mode 100644 plugins/main/public/controllers/management/components/management/reporting/utils/actions-buttons-main.js delete mode 100644 plugins/main/public/controllers/management/components/management/reporting/utils/columns-main.js delete mode 100644 plugins/main/public/controllers/management/components/management/reporting/utils/reporting-handler.js diff --git a/plugins/main/public/controllers/management/components/management/management-main.js b/plugins/main/public/controllers/management/components/management/management-main.js index 3a6e5982b7..6fdcd168fd 100644 --- a/plugins/main/public/controllers/management/components/management/management-main.js +++ b/plugins/main/public/controllers/management/components/management/management-main.js @@ -19,7 +19,6 @@ import WzDecoders from './decoders/main-decoders'; import WzGroups from './groups/groups-main'; import WzStatus from './status/status-main'; import WzLogs from './mg-logs/logs'; -import WzReporting from './reporting/reporting-main'; import WzConfiguration from './configuration/configuration-main'; import WzStatistics from './statistics/statistics-main'; import { @@ -41,7 +40,6 @@ class WzManagementMain extends Component { {(section === 'groups' && ) || (section === 'status' && ) || - (section === 'reporting' && ) || (section === 'statistics' && ) || (section === 'logs' && ) || (section === 'configuration' && ( diff --git a/plugins/main/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap b/plugins/main/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap deleted file mode 100644 index 5941b82a4a..0000000000 --- a/plugins/main/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Reporting component renders correctly to match the snapshot 1`] = ` - - - -`; diff --git a/plugins/main/public/controllers/management/components/management/reporting/reporting-main.js b/plugins/main/public/controllers/management/components/management/reporting/reporting-main.js deleted file mode 100644 index 8d64ac9915..0000000000 --- a/plugins/main/public/controllers/management/components/management/reporting/reporting-main.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Wazuh app - React component for reporting - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component } from 'react'; - -import WzReduxProvider from '../../../../../redux/wz-redux-provider'; -//Wazuh groups overview -import WzReportingOverview from './reporting-overview'; -import { compose } from 'redux'; -import { - withGlobalBreadcrumb, - withReduxProvider, -} from '../../../../../components/common/hocs'; -import { reporting } from '../../../../../utils/applications'; - -class WzReporting extends Component { - constructor(props) { - super(props); - this.state = {}; - } - - render() { - return ( - - - - ); - } -} - -export default compose( - withReduxProvider, - withGlobalBreadcrumb(props => { - return [{ text: reporting.breadcrumbLabel }]; - }), -)(WzReporting); diff --git a/plugins/main/public/controllers/management/components/management/reporting/reporting-main.test.tsx b/plugins/main/public/controllers/management/components/management/reporting/reporting-main.test.tsx deleted file mode 100644 index 25bb567001..0000000000 --- a/plugins/main/public/controllers/management/components/management/reporting/reporting-main.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Wazuh app - React test for Reporting component. - * - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - * - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import WzReporting from './reporting-main'; - -jest.mock('../../../../../kibana-services', () => ({ - getAngularModule: jest.fn(), - getHttp: () => ({ - basePath: { - prepend: (str) => str, - }, - }), -})); - -describe('Reporting component', () => { - it('renders correctly to match the snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/plugins/main/public/controllers/management/components/management/reporting/reporting-overview.js b/plugins/main/public/controllers/management/components/management/reporting/reporting-overview.js deleted file mode 100644 index aa093b0d1a..0000000000 --- a/plugins/main/public/controllers/management/components/management/reporting/reporting-overview.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Wazuh app - React component for building the reporting view - * - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component } from 'react'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiPanel, - EuiTitle, - EuiPage, - EuiText -} from '@elastic/eui'; - -// Wazuh components -import WzReportingTable from './reporting-table'; -import WzReportingActionButtons from './utils/actions-buttons-main'; - -export class WzReportingOverview extends Component { - _isMounted = false; - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { - this._isMounted = true; - } - - componentDidUpdate() {} - - componentWillUnmount() { - this._isMounted = false; - } - - render() { - return ( - - - - - - - -

Reporting

-
-
-
-
- -
- - - - From here you can check all your reports. - - - - - - - - -
-
- ); - } -} - -export default WzReportingOverview; diff --git a/plugins/main/public/controllers/management/components/management/reporting/reporting-table.js b/plugins/main/public/controllers/management/components/management/reporting/reporting-table.js deleted file mode 100644 index 53aa7e6801..0000000000 --- a/plugins/main/public/controllers/management/components/management/reporting/reporting-table.js +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Wazuh app - React component for reporting table. - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component } from 'react'; -import { EuiInMemoryTable, EuiCallOut, EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; - -import { connect } from 'react-redux'; -import ReportingHandler from './utils/reporting-handler'; -import { getToasts } from '../../../../../kibana-services'; - -import { - updateIsProcessing, - updateShowModal, - updateListItemsForRemove, -} from '../../../../../redux/actions/reportingActions'; - -import ReportingColums from './utils/columns-main'; - -import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -import { getErrorOrchestrator } from '../../../../../react-services/common-services'; - -class WzReportingTable extends Component { - _isMounted = false; - constructor(props) { - super(props); - this.state = { - items: [], - isLoading: false, - }; - - this.reportingHandler = ReportingHandler; - } - - async componentDidMount() { - this.props.updateIsProcessing(true); - this._isMounted = true; - } - - async componentDidUpdate() { - try { - if (this.props.state.isProcessing && this._isMounted) { - await this.getItems(); - } - } catch (error) { - const options = { - context: `${WzReportingTable.name}.componentDidUpdate`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - componentWillUnmount() { - this._isMounted = false; - } - - /** - * Loads the initial information - */ - async getItems() { - try { - const rawItems = await this.reportingHandler.listReports(); - const {reports: items = []} = rawItems?.data; - this.setState({ - items, - isProcessing: false, - }); - this.props.updateIsProcessing(false); - } catch (error) { - this.props.updateIsProcessing(false); - throw error; - } - } - - render() { - this.reportingColumns = new ReportingColums(this.props); - const { isLoading, error } = this.props.state; - const { items } = this.state; - const columns = this.reportingColumns.columns; - const message = isLoading ? null : 'No results...'; - - const deleteReport = (itemList) => { - try { - this.deleteReport(itemList); - this.props.updateShowModal(false); - } catch (error) { - const options = { - context: `${WzReportingTable.name}.deleteReport`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error deleting report`, - }, - }; - getErrorOrchestrator().handleError(options); - } - }; - - const sorting = { - sort: { - field: 'date', - direction: 'desc', - }, - }; - - if (!error) { - const itemList = this.props.state.itemList; - return ( -
- - {this.props.state.showModal ? ( - - this.props.updateShowModal(false)} - onConfirm={() => deleteReport(itemList)} - cancelButtonText="Cancel" - confirmButtonText="Delete" - defaultFocusedButton="cancel" - buttonColor="danger" - > - - ) : null} -
- ); - } else { - return ; - } - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - - async deleteReport(items) { - try { - const results = items.map(async (item, i) => { - await this.reportingHandler.deleteReport(item.name); - }); - await Promise.all(results); - this.props.updateIsProcessing(true); - this.showToast('success', 'Success', 'Deleted successfully', 3000); - } catch (error) { - throw error; - } - } -} - -const mapStateToProps = (state) => { - return { - state: state.reportingReducers, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), - updateShowModal: (showModal) => dispatch(updateShowModal(showModal)), - updateListItemsForRemove: (itemList) => dispatch(updateListItemsForRemove(itemList)), - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(WzReportingTable); diff --git a/plugins/main/public/controllers/management/components/management/reporting/utils/actions-buttons-main.js b/plugins/main/public/controllers/management/components/management/reporting/utils/actions-buttons-main.js deleted file mode 100644 index 92c70bc926..0000000000 --- a/plugins/main/public/controllers/management/components/management/reporting/utils/actions-buttons-main.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Wazuh app - React component for reporting buttons - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component, Fragment } from 'react'; -// Eui components -import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; - -import { connect } from 'react-redux'; - -import { updateIsProcessing } from '../../../../../../redux/actions/reportingActions'; - -import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; -import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; - -class WzReportingActionButtons extends Component { - _isMounted = false; - - constructor(props) { - super(props); - } - - componentDidMount() { - this._isMounted = true; - } - - componentDidUpdate() {} - - componentWillUnmount() { - this._isMounted = false; - } - - /** - * Refresh the items - */ - async refresh() { - try { - this.props.updateIsProcessing(true); - } catch (error) { - const options = { - context: `${WzReportingActionButtons.name}.refresh`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.UI, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - render() { - // Refresh - const refreshButton = ( - await this.refresh()} - > - Refresh - - ); - - return ( - - {refreshButton} - - ); - } -} - -const mapStateToProps = state => { - return { - state: state.reportingReducers - }; -}; - -const mapDispatchToProps = dispatch => { - return { - updateIsProcessing: isProcessing => - dispatch(updateIsProcessing(isProcessing)) - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzReportingActionButtons); diff --git a/plugins/main/public/controllers/management/components/management/reporting/utils/columns-main.js b/plugins/main/public/controllers/management/components/management/reporting/utils/columns-main.js deleted file mode 100644 index b8d5d8358c..0000000000 --- a/plugins/main/public/controllers/management/components/management/reporting/utils/columns-main.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; -import ReportingHandler from './reporting-handler'; -import moment from 'moment-timezone'; -import { WzButtonPermissions } from '../../../../../../components/common/permissions/button'; -import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../../../../../common/constants'; -import { getHttp, getUiSettings } from '../../../../../../kibana-services'; -import { formatUIDate } from '../../../../../../react-services/time-service'; -export default class ReportingColums { - constructor(tableProps) { - this.tableProps = tableProps; - this.reportingHandler = ReportingHandler; - - this.buildColumns = () => { - this.columns = [ - { - field: 'name', - name: 'File', - align: 'left', - sortable: true - }, - { - field: 'size', - name: 'Size', - render: size => { - const fixedSize = size / 1024; - return `${fixedSize.toFixed(2)}KB`; - }, - sortable: true - }, - { - field: 'date', - name: 'Created', - render: value => formatUIDate(value), - sortable: true - } - ]; - this.columns.push({ - name: 'Actions', - align: 'left', - render: item => { - return ( -
- - this.goReport(item.name)} - color="primary" - /> - - - { - this.tableProps.updateListItemsForRemove([item]); - this.tableProps.updateShowModal(true); - }} - color="danger" - isDisabled={item.name === 'default'} - /> -
- ); - } - }); - }; - - this.buildColumns(); - } - - /** - * Downloads the report - * @param {*} name The name of the report - */ - goReport(name) { - window.open(getHttp().basePath.prepend(`/reports/${name}`), '_blank'); - } - - /** - * Returns given date adding the timezone offset - * @param {string} date Date - */ - offset(d) { - try { - const dateUTC = moment.utc(d); - const kibanaTz = getUiSettings().get('dateFormat:tz'); - const dateLocate = - kibanaTz === 'Browser' - ? moment(dateUTC).local() - : moment(dateUTC).tz(kibanaTz); - return dateLocate.format('YYYY/MM/DD HH:mm:ss'); - } catch (error) { - throw new Error(error); - } - } -} diff --git a/plugins/main/public/controllers/management/components/management/reporting/utils/reporting-handler.js b/plugins/main/public/controllers/management/components/management/reporting/utils/reporting-handler.js deleted file mode 100644 index e728fd9066..0000000000 --- a/plugins/main/public/controllers/management/components/management/reporting/utils/reporting-handler.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Wazuh app - Reporting handler service - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import { WzRequest } from '../../../../../../react-services/wz-request'; - -export default class ReportingHandler { - /** - * Get list reports - * @param {String} name - */ - static async listReports() { - try { - const result = await WzRequest.genericReq('GET', '/reports'); - return result; - } catch (error) { - throw error; - } - } - - /** - * Delete report - * @param {String} name - */ - static async deleteReport(name) { - try { - const result = await WzRequest.genericReq( - 'DELETE', - `/reports/${name}` - ); - return result; - } catch (error) { - throw error; - } - } -} diff --git a/plugins/main/public/utils/applications.ts b/plugins/main/public/utils/applications.ts index e9a82082ac..47377dc813 100644 --- a/plugins/main/public/utils/applications.ts +++ b/plugins/main/public/utils/applications.ts @@ -656,25 +656,6 @@ export const logs = { redirectTo: () => '/manager/?tab=logs', }; -export const reporting = { - category: 'wz-category-dashboard-management', - id: 'reporting', - title: i18n.translate('wz-app-reporting-title', { - defaultMessage: 'Reporting', - }), - breadcrumbLabel: i18n.translate('wz-app-reporting-breadcrumbLabel', { - defaultMessage: 'Reporting', - }), - description: i18n.translate('wz-app-reporting-description', { - defaultMessage: 'Check your stored Wazuh reports.', - }), - euiIconType: 'indexRollupApp', - order: 702, - showInOverviewApp: false, - showInAgentMenu: false, - redirectTo: () => '/manager/?tab=reporting', -}; - export const settings = { category: 'wz-category-server-management', id: 'settings', @@ -860,7 +841,6 @@ export const Applications = [ statistics, logs, settings, - reporting, serverApis, sampleData, appSettings, From 49a550e22e0cdba4f7649d7606b5a3d2aa8536ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 7 Mar 2024 17:06:08 +0100 Subject: [PATCH 03/12] feat(reporting): remove API endpoints related to the management of PDF reports files - Remove API endpoints related to the management of PDF reports files - GET /reports - GET /reports/{name} - DELETE /reports --- .../server/controllers/wazuh-reporting.ts | 124 --------- plugins/main/server/routes/wazuh-reporting.ts | 237 +++++++++--------- 2 files changed, 116 insertions(+), 245 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-reporting.ts b/plugins/main/server/controllers/wazuh-reporting.ts index b046a4c515..b03d688cd4 100644 --- a/plugins/main/server/controllers/wazuh-reporting.ts +++ b/plugins/main/server/controllers/wazuh-reporting.ts @@ -10,9 +10,7 @@ * Find more information about this on the LICENSE file. */ import path from 'path'; -import fs from 'fs'; import { WAZUH_MODULES } from '../../common/wazuh-modules'; -import * as TimSort from 'timsort'; import { ErrorResponse } from '../lib/error-response'; import ProcessEquivalence from '../lib/process-state-equivalence'; import { KeyEquivalence } from '../../common/csv-key-equivalence'; @@ -1330,128 +1328,6 @@ export class WazuhReportingCtrl { `wazuh-agent-inventory-${agentID}-${this.generateReportTimestamp()}.pdf`, ); - /** - * Fetch the reports list - * @param {Object} context - * @param {Object} request - * @param {Object} response - * @returns {Array} reports list or ErrorResponse - */ - async getReports( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) { - try { - context.wazuh.logger.debug('Fetching created reports'); - const { hashUsername } = await context.wazuh.security.getCurrentUser( - request, - context, - ); - createDataDirectoryIfNotExists(); - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH); - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH); - const userReportsDirectoryPath = path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - hashUsername, - ); - createDirectoryIfNotExists(userReportsDirectoryPath); - context.wazuh.logger.debug(`Directory: ${userReportsDirectoryPath}`); - - const sortReportsByDate = (a, b) => - a.date < b.date ? 1 : a.date > b.date ? -1 : 0; - - const reports = fs.readdirSync(userReportsDirectoryPath).map(file => { - const stats = fs.statSync(userReportsDirectoryPath + '/' + file); - // Get the file creation time (bithtime). It returns the first value that is a truthy value of next file stats: birthtime, mtime, ctime and atime. - // This solves some OSs can have the bithtimeMs equal to 0 and returns the date like 1970-01-01 - const birthTimeField = ['birthtime', 'mtime', 'ctime', 'atime'].find( - time => stats[`${time}Ms`], - ); - return { - name: file, - size: stats.size, - date: stats[birthTimeField], - }; - }); - context.wazuh.logger.debug( - `Using TimSort for sorting ${reports.length} items`, - ); - TimSort.sort(reports, sortReportsByDate); - context.wazuh.logger.debug(`Total reports: ${reports.length}`); - return response.ok({ - body: { reports }, - }); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse(error.message || error, 5031, 500, response); - } - } - - /** - * Fetch specific report - * @param {Object} context - * @param {Object} request - * @param {Object} response - * @returns {Object} report or ErrorResponse - */ - getReportByName = this.checkReportsUserDirectoryIsValidRouteDecorator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - context.wazuh.logger.debug( - `Getting ${context.wazuhEndpointParams.pathFilename} report`, - ); - const reportFileBuffer = fs.readFileSync( - context.wazuhEndpointParams.pathFilename, - ); - return response.ok({ - headers: { 'Content-Type': 'application/pdf' }, - body: reportFileBuffer, - }); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse(error.message || error, 5030, 500, response); - } - }, - request => request.params.name, - ); - - /** - * Delete specific report - * @param {Object} context - * @param {Object} request - * @param {Object} response - * @returns {Object} status obj or ErrorResponse - */ - deleteReportByName = this.checkReportsUserDirectoryIsValidRouteDecorator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - context.wazuh.logger.debug( - `Deleting ${context.wazuhEndpointParams.pathFilename} report`, - ); - fs.unlinkSync(context.wazuhEndpointParams.pathFilename); - context.wazuh.logger.info( - `${context.wazuhEndpointParams.pathFilename} report was deleted`, - ); - return response.ok({ - body: { error: 0 }, - }); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse(error.message || error, 5032, 500, response); - } - }, - request => request.params.name, - ); - checkReportsUserDirectoryIsValidRouteDecorator( routeHandler, reportFileNameAccessor, diff --git a/plugins/main/server/routes/wazuh-reporting.ts b/plugins/main/server/routes/wazuh-reporting.ts index 7f78a27458..a150470705 100644 --- a/plugins/main/server/routes/wazuh-reporting.ts +++ b/plugins/main/server/routes/wazuh-reporting.ts @@ -18,16 +18,23 @@ export function WazuhReportingRoutes(router: IRouter) { const agentIDValidation = schema.string({ minLength: 3, - validate: (agentID: string) => /^\d{3,}$/.test(agentID) ? undefined : 'must be 0-9 are allowed' + validate: (agentID: string) => + /^\d{3,}$/.test(agentID) ? undefined : 'must be 0-9 are allowed', }); const groupIDValidation = schema.string({ minLength: 1, - validate: (agentID: string) => /^(?!^(\.{1,2}|all)$)[\w\.\-]+$/.test(agentID) ? undefined : 'must be A-z, 0-9, _, . are allowed. It must not be ., .. or all.' + validate: (agentID: string) => + /^(?!^(\.{1,2}|all)$)[\w\.\-]+$/.test(agentID) + ? undefined + : 'must be A-z, 0-9, _, . are allowed. It must not be ., .. or all.', }); const ReportFilenameValidation = schema.string({ - validate: (agentID: string) => /^[\w\-\.]+\.pdf$/.test(agentID) ? undefined : 'must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.' + validate: (agentID: string) => + /^[\w\-\.]+\.pdf$/.test(agentID) + ? undefined + : 'must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.', }); const moduleIDValidation = schema.oneOf([ @@ -54,129 +61,117 @@ export function WazuhReportingRoutes(router: IRouter) { schema.literal('tsc'), ]); - router.post({ - path: '/reports/modules/{moduleID}', - validate: { - body: schema.object({ - array: schema.any(), - browserTimezone: schema.string(), - serverSideQuery: schema.maybe(schema.any()), - filters: schema.maybe(schema.any()), - agents: schema.maybe(schema.oneOf([agentIDValidation, schema.boolean()])), - components: schema.maybe(schema.any()), - searchBar: schema.maybe(schema.string()), - section: schema.maybe(schema.string()), - tab: schema.string(), - tables: schema.maybe(schema.any()), - time: schema.oneOf([schema.object({ - from: schema.string(), - to: schema.string() - }), schema.string()]), - indexPatternTitle: schema.string(), - apiId: schema.string() - }), - params: schema.object({ - moduleID: moduleIDValidation - }) - } - }, - (context, request, response) => ctrl.createReportsModules(context, request, response) + router.post( + { + path: '/reports/modules/{moduleID}', + validate: { + body: schema.object({ + array: schema.any(), + browserTimezone: schema.string(), + serverSideQuery: schema.maybe(schema.any()), + filters: schema.maybe(schema.any()), + agents: schema.maybe( + schema.oneOf([agentIDValidation, schema.boolean()]), + ), + components: schema.maybe(schema.any()), + searchBar: schema.maybe(schema.string()), + section: schema.maybe(schema.string()), + tab: schema.string(), + tables: schema.maybe(schema.any()), + time: schema.oneOf([ + schema.object({ + from: schema.string(), + to: schema.string(), + }), + schema.string(), + ]), + indexPatternTitle: schema.string(), + apiId: schema.string(), + }), + params: schema.object({ + moduleID: moduleIDValidation, + }), + }, + }, + (context, request, response) => + ctrl.createReportsModules(context, request, response), ); - router.post({ - path: '/reports/groups/{groupID}', - validate: { - body: schema.object({ - browserTimezone: schema.string(), - filters: schema.maybe(schema.any()), - components: schema.maybe(schema.any()), - section: schema.maybe(schema.string()), - apiId: schema.string() - }), - params: schema.object({ - groupID: groupIDValidation - }) - } - }, - (context, request, response) => ctrl.createReportsGroups(context, request, response) + router.post( + { + path: '/reports/groups/{groupID}', + validate: { + body: schema.object({ + browserTimezone: schema.string(), + filters: schema.maybe(schema.any()), + components: schema.maybe(schema.any()), + section: schema.maybe(schema.string()), + apiId: schema.string(), + }), + params: schema.object({ + groupID: groupIDValidation, + }), + }, + }, + (context, request, response) => + ctrl.createReportsGroups(context, request, response), ); - router.post({ - path: '/reports/agents/{agentID}', - validate: { - body: schema.object({ - browserTimezone: schema.string(), - filters: schema.any(), - components: schema.maybe(schema.any()), - section: schema.maybe(schema.string()), - apiId: schema.string() - }), - params: schema.object({ - agentID: agentIDValidation - }) - } - }, - (context, request, response) => ctrl.createReportsAgentsConfiguration(context, request, response) + router.post( + { + path: '/reports/agents/{agentID}', + validate: { + body: schema.object({ + browserTimezone: schema.string(), + filters: schema.any(), + components: schema.maybe(schema.any()), + section: schema.maybe(schema.string()), + apiId: schema.string(), + }), + params: schema.object({ + agentID: agentIDValidation, + }), + }, + }, + (context, request, response) => + ctrl.createReportsAgentsConfiguration(context, request, response), ); - router.post({ - path: '/reports/agents/{agentID}/inventory', - validate: { - body: schema.object({ - array: schema.any(), - browserTimezone: schema.string(), - serverSideQuery: schema.maybe(schema.any()), - filters: schema.maybe(schema.any()), - agents: schema.maybe(schema.oneOf([schema.string(), schema.boolean()])), - components: schema.maybe(schema.any()), - searchBar: schema.maybe(schema.oneOf([schema.string(), schema.boolean()])), - section: schema.maybe(schema.string()), - tab: schema.string(), - tables: schema.maybe(schema.any()), - time: schema.oneOf([schema.object({ - from: schema.string(), - to: schema.string() - }), schema.string()]), - indexPatternTitle: schema.string(), - apiId: schema.string() - }), - params: schema.object({ - agentID: agentIDValidation - }) - } - }, - (context, request, response) => ctrl.createReportsAgentsInventory(context, request, response) - ); - - // Fetch specific report - router.get({ - path: '/reports/{name}', - validate: { - params: schema.object({ - name: ReportFilenameValidation - }) - } - }, - (context, request, response) => ctrl.getReportByName(context, request, response) - ); - - // Delete specific report - router.delete({ - path: '/reports/{name}', - validate: { - params: schema.object({ - name: ReportFilenameValidation - }) - } - }, - (context, request, response) => ctrl.deleteReportByName(context, request, response) - ) - - // Fetch the reports list - router.get({ - path: '/reports', - validate: false - }, - (context, request, response) => ctrl.getReports(context, request, response) + router.post( + { + path: '/reports/agents/{agentID}/inventory', + validate: { + body: schema.object({ + array: schema.any(), + browserTimezone: schema.string(), + serverSideQuery: schema.maybe(schema.any()), + filters: schema.maybe(schema.any()), + agents: schema.maybe( + schema.oneOf([schema.string(), schema.boolean()]), + ), + components: schema.maybe(schema.any()), + searchBar: schema.maybe( + schema.oneOf([schema.string(), schema.boolean()]), + ), + section: schema.maybe(schema.string()), + tab: schema.string(), + tables: schema.maybe(schema.any()), + time: schema.oneOf([ + schema.object({ + from: schema.string(), + to: schema.string(), + }), + schema.string(), + ]), + indexPatternTitle: schema.string(), + apiId: schema.string(), + }), + params: schema.object({ + agentID: agentIDValidation, + }), + }, + }, + (context, request, response) => + ctrl.createReportsAgentsInventory(context, request, response), ); } From 358c8241a969409c88f25cca8efdaf96ebe95bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 8 Mar 2024 08:35:45 +0100 Subject: [PATCH 04/12] fix: manage errors on request when using responseType: blob --- .../main/public/services/request-handler.js | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/plugins/main/public/services/request-handler.js b/plugins/main/public/services/request-handler.js index 80d2a183fa..1ea22638fe 100644 --- a/plugins/main/public/services/request-handler.js +++ b/plugins/main/public/services/request-handler.js @@ -2,16 +2,16 @@ import axios from 'axios'; import { HTTP_STATUS_CODES } from '../../common/constants'; let allow = true; -export let unregisterInterceptor = () => { }; +export let unregisterInterceptor = () => {}; const source = axios.CancelToken.source(); const disableRequests = () => { allow = false; source.cancel('Requests cancelled'); return; -} +}; -export const initializeInterceptor = (core) => { +export const initializeInterceptor = core => { unregisterInterceptor = core.http.intercept({ responseError: (httpErrorResponse, controller) => { if ( @@ -21,37 +21,47 @@ export const initializeInterceptor = (core) => { } }, request: (current, controller) => { - if ( - !allow - ) { - throw new Error("Disable request"); - }; + if (!allow) { + throw new Error('Disable request'); + } }, }); -} +}; export const request = async (options = {}) => { if (!allow) { return Promise.reject('Requests are disabled'); - }; + } if (!options.method || !options.url) { - return Promise.reject("Missing parameters"); - }; + return Promise.reject('Missing parameters'); + } options = { - ...options, cancelToken: source?.token + ...options, + cancelToken: source?.token, }; if (allow) { try { const requestData = await axios(options); return Promise.resolve(requestData); - } - catch (e) { - if (e.response?.data?.message === 'Unauthorized' || e.response?.data?.message === 'Authentication required') { + } catch (e) { + /* Transform the response when the data is comming as a blob to POJO. + Using the responseType: blob causes the response data is formatted to a blob, despite + the response can be a json. */ + if ( + e.response?.data?.constructor?.name === 'Blob' && + e?.response?.headers?.['content-type']?.includes?.('application/json') + ) { + e.response.data = JSON.parse(await e.response.data.text()); + } + if ( + e.response?.data?.message === 'Unauthorized' || + e.response?.data?.message === 'Authentication required' + ) { disableRequests(); window.location.reload(); } return Promise.reject(e); } } -} +}; From ef2a080db4a92e7b7a3e0f9472b7f44afa85122b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 8 Mar 2024 08:50:53 +0100 Subject: [PATCH 05/12] feat(reporting): remove route decorator to ensure the path to the report file is valid --- .../server/controllers/wazuh-reporting.ts | 1816 ++++++++--------- 1 file changed, 843 insertions(+), 973 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-reporting.ts b/plugins/main/server/controllers/wazuh-reporting.ts index b03d688cd4..e2cc6b8ee3 100644 --- a/plugins/main/server/controllers/wazuh-reporting.ts +++ b/plugins/main/server/controllers/wazuh-reporting.ts @@ -295,113 +295,102 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsModules = this.checkReportsUserDirectoryIsValidRouteDecorator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - context.wazuh.logger.debug('Report started'); - const { - array, - agents, - browserTimezone, - searchBar, - filters, - serverSideQuery, - time, - tables, - section, - indexPatternTitle, - apiId, - } = request.body; - const { moduleID } = request.params; - const { from, to } = time || {}; - let additionalTables = []; - // Init - const printer = new ReportPrinter( - context.wazuh.logger.get('report-printer'), - ); + async createReportsModules( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + ) { + try { + context.wazuh.logger.debug('Report started'); + const { + array, + agents, + browserTimezone, + searchBar, + filters, + serverSideQuery, + time, + tables, + section, + indexPatternTitle, + apiId, + } = request.body; + const { moduleID } = request.params; + const { from, to } = time || {}; + let additionalTables = []; + // Init + const printer = new ReportPrinter( + context.wazuh.logger.get('report-printer'), + ); + + await this.renderHeader( + context, + printer, + section, + moduleID, + agents, + apiId, + ); + + const [sanitizedFilters, agentsFilter] = filters + ? this.sanitizeKibanaFilters(context, filters, searchBar) + : [false, null]; - createDataDirectoryIfNotExists(); - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH); - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH); - createDirectoryIfNotExists( - path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - context.wazuhEndpointParams.hashUsername, - ), + if (time && sanitizedFilters) { + printer.addTimeRangeAndFilters( + from, + to, + sanitizedFilters, + browserTimezone, ); + } - await this.renderHeader( + if (time) { + additionalTables = await extendedInformation( context, printer, section, moduleID, - agents, apiId, + new Date(from).getTime(), + new Date(to).getTime(), + serverSideQuery, + agentsFilter, + indexPatternTitle, + agents, ); + } - const [sanitizedFilters, agentsFilter] = filters - ? this.sanitizeKibanaFilters(context, filters, searchBar) - : [false, null]; - - if (time && sanitizedFilters) { - printer.addTimeRangeAndFilters( - from, - to, - sanitizedFilters, - browserTimezone, - ); - } - - if (time) { - additionalTables = await extendedInformation( - context, - printer, - section, - moduleID, - apiId, - new Date(from).getTime(), - new Date(to).getTime(), - serverSideQuery, - agentsFilter, - indexPatternTitle, - agents, - ); - } - - printer.addVisualizations(array, agents, moduleID); + printer.addVisualizations(array, agents, moduleID); - if (tables) { - printer.addTables([...tables, ...(additionalTables || [])]); - } - - //add authorized agents - if (agentsFilter?.agentsText) { - printer.addAgentsFilters(agentsFilter.agentsText); - } - - const buffer = await printer.print(); + if (tables) { + printer.addTables([...tables, ...(additionalTables || [])]); + } - return response.ok({ - headers: { - 'Content-Type': 'application/pdf', - 'Content-Disposition': `attachment;filename="${context.wazuhEndpointParams.filename}"`, - 'Cache-Control': 'no-store,no-cache', - }, - body: buffer, - }); - } catch (error) { - return ErrorResponse(error.message || error, 5029, 500, response); + //add authorized agents + if (agentsFilter?.agentsText) { + printer.addAgentsFilters(agentsFilter.agentsText); } - }, - ({ body: { agents }, params: { moduleID } }) => - `wazuh-module-${ - agents ? `agents-${agents}` : 'overview' - }-${moduleID}-${this.generateReportTimestamp()}.pdf`, - ); + + const buffer = await printer.print(); + const filename = `wazuh-module-${ + request.body.agents + ? `agents-${request.body.agents.agents}` + : 'overview' + }-${request.params.moduleID}-${this.generateReportTimestamp()}.pdf`; + + return response.ok({ + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment;filename="${filename}"`, + 'Cache-Control': 'no-store,no-cache', + }, + body: buffer, + }); + } catch (error) { + return ErrorResponse(error.message || error, 5029, 500, response); + } + } /** * Create a report for the groups @@ -410,169 +399,135 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsGroups = this.checkReportsUserDirectoryIsValidRouteDecorator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - context.wazuh.logger.debug('Report started'); - const { components, apiId } = request.body; - const { groupID } = request.params; - // Init - const printer = new ReportPrinter( - context.wazuh.logger.get('report-printer'), - ); - - createDataDirectoryIfNotExists(); - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH); - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH); - createDirectoryIfNotExists( - path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - context.wazuhEndpointParams.hashUsername, - ), - ); + async createReportsGroups( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + ) { + try { + context.wazuh.logger.debug('Report started'); + const { components, apiId } = request.body; + const { groupID } = request.params; + // Init + const printer = new ReportPrinter( + context.wazuh.logger.get('report-printer'), + ); - let tables = []; - const equivalences = { - localfile: 'Local files', - osquery: 'Osquery', - command: 'Command', - syscheck: 'Syscheck', - 'open-scap': 'OpenSCAP', - 'cis-cat': 'CIS-CAT', - syscollector: 'Syscollector', - rootcheck: 'Rootcheck', - labels: 'Labels', - sca: 'Security configuration assessment', - }; - printer.addContent({ - text: `Group ${groupID} configuration`, - style: 'h1', - }); + let tables = []; + const equivalences = { + localfile: 'Local files', + osquery: 'Osquery', + command: 'Command', + syscheck: 'Syscheck', + 'open-scap': 'OpenSCAP', + 'cis-cat': 'CIS-CAT', + syscollector: 'Syscollector', + rootcheck: 'Rootcheck', + labels: 'Labels', + sca: 'Security configuration assessment', + }; + printer.addContent({ + text: `Group ${groupID} configuration`, + style: 'h1', + }); - // Group configuration - if (components['0']) { - const { - data: { data: configuration }, - } = await context.wazuh.api.client.asCurrentUser.request( - 'GET', - `/groups/${groupID}/configuration`, - {}, - { apiHostID: apiId }, - ); + // Group configuration + if (components['0']) { + const { + data: { data: configuration }, + } = await context.wazuh.api.client.asCurrentUser.request( + 'GET', + `/groups/${groupID}/configuration`, + {}, + { apiHostID: apiId }, + ); - if ( - configuration.affected_items.length > 0 && - Object.keys(configuration.affected_items[0].config).length - ) { + if ( + configuration.affected_items.length > 0 && + Object.keys(configuration.affected_items[0].config).length + ) { + printer.addContent({ + text: 'Configurations', + style: { fontSize: 14, color: '#000' }, + margin: [0, 10, 0, 15], + }); + const section = { + labels: [], + isGroupConfig: true, + }; + for (let config of configuration.affected_items) { + let filterTitle = ''; + let index = 0; + for (let filter of Object.keys(config.filters)) { + filterTitle = filterTitle.concat( + `${filter}: ${config.filters[filter]}`, + ); + if (index < Object.keys(config.filters).length - 1) { + filterTitle = filterTitle.concat(' | '); + } + index++; + } printer.addContent({ - text: 'Configurations', - style: { fontSize: 14, color: '#000' }, - margin: [0, 10, 0, 15], + text: filterTitle, + style: 'h4', + margin: [0, 0, 0, 10], }); - const section = { - labels: [], - isGroupConfig: true, - }; - for (let config of configuration.affected_items) { - let filterTitle = ''; - let index = 0; - for (let filter of Object.keys(config.filters)) { - filterTitle = filterTitle.concat( - `${filter}: ${config.filters[filter]}`, - ); - if (index < Object.keys(config.filters).length - 1) { - filterTitle = filterTitle.concat(' | '); - } - index++; - } - printer.addContent({ - text: filterTitle, - style: 'h4', - margin: [0, 0, 0, 10], - }); - let idx = 0; - section.tabs = []; - for (let _d of Object.keys(config.config)) { - for (let c of AgentConfiguration.configurations) { - for (let s of c.sections) { - section.opts = s.opts || {}; - for (let cn of s.config || []) { - if (cn.configuration === _d) { - section.labels = s.labels || [[]]; - } + let idx = 0; + section.tabs = []; + for (let _d of Object.keys(config.config)) { + for (let c of AgentConfiguration.configurations) { + for (let s of c.sections) { + section.opts = s.opts || {}; + for (let cn of s.config || []) { + if (cn.configuration === _d) { + section.labels = s.labels || [[]]; } - for (let wo of s.wodle || []) { - if (wo.name === _d) { - section.labels = s.labels || [[]]; - } + } + for (let wo of s.wodle || []) { + if (wo.name === _d) { + section.labels = s.labels || [[]]; } } } - section.labels[0]['pack'] = 'Packs'; - section.labels[0]['content'] = 'Evaluations'; - section.labels[0]['7'] = 'Scan listening netwotk ports'; - section.tabs.push(equivalences[_d]); - - if (Array.isArray(config.config[_d])) { - /* LOG COLLECTOR */ - if (_d === 'localfile') { - let groups = []; - config.config[_d].forEach(obj => { - if (!groups[obj.logformat]) { - groups[obj.logformat] = []; + } + section.labels[0]['pack'] = 'Packs'; + section.labels[0]['content'] = 'Evaluations'; + section.labels[0]['7'] = 'Scan listening netwotk ports'; + section.tabs.push(equivalences[_d]); + + if (Array.isArray(config.config[_d])) { + /* LOG COLLECTOR */ + if (_d === 'localfile') { + let groups = []; + config.config[_d].forEach(obj => { + if (!groups[obj.logformat]) { + groups[obj.logformat] = []; + } + groups[obj.logformat].push(obj); + }); + Object.keys(groups).forEach(group => { + let saveidx = 0; + groups[group].forEach((x, i) => { + if ( + Object.keys(x).length > + Object.keys(groups[group][saveidx]).length + ) { + saveidx = i; } - groups[obj.logformat].push(obj); - }); - Object.keys(groups).forEach(group => { - let saveidx = 0; - groups[group].forEach((x, i) => { - if ( - Object.keys(x).length > - Object.keys(groups[group][saveidx]).length - ) { - saveidx = i; - } - }); - const columns = Object.keys(groups[group][saveidx]); - const rows = groups[group].map(x => { - let row = []; - columns.forEach(key => { - row.push( - typeof x[key] !== 'object' - ? x[key] - : Array.isArray(x[key]) - ? x[key].map(x => { - return x + '\n'; - }) - : JSON.stringify(x[key]), - ); - }); - return row; - }); - columns.forEach((col, i) => { - columns[i] = col[0].toUpperCase() + col.slice(1); - }); - tables.push({ - title: 'Local files', - type: 'table', - columns, - rows, - }); }); - } else if (_d === 'labels') { - const obj = config.config[_d][0].label; - const columns = Object.keys(obj[0]); - if (!columns.includes('hidden')) { - columns.push('hidden'); - } - const rows = obj.map(x => { + const columns = Object.keys(groups[group][saveidx]); + const rows = groups[group].map(x => { let row = []; columns.forEach(key => { - row.push(x[key]); + row.push( + typeof x[key] !== 'object' + ? x[key] + : Array.isArray(x[key]) + ? x[key].map(x => { + return x + '\n'; + }) + : JSON.stringify(x[key]), + ); }); return row; }); @@ -580,122 +535,145 @@ export class WazuhReportingCtrl { columns[i] = col[0].toUpperCase() + col.slice(1); }); tables.push({ - title: 'Labels', + title: 'Local files', type: 'table', columns, rows, }); - } else { - for (let _d2 of config.config[_d]) { - tables.push( - ...this.getConfigTables(context, _d2, section, idx), - ); - } + }); + } else if (_d === 'labels') { + const obj = config.config[_d][0].label; + const columns = Object.keys(obj[0]); + if (!columns.includes('hidden')) { + columns.push('hidden'); } - } else { - /*INTEGRITY MONITORING MONITORED DIRECTORIES */ - if (config.config[_d].directories) { - const directories = config.config[_d].directories; - delete config.config[_d].directories; - tables.push( - ...this.getConfigTables( - context, - config.config[_d], - section, - idx, - ), - ); - let diffOpts = []; - Object.keys(section.opts).forEach(x => { - diffOpts.push(x); - }); - const columns = [ - '', - ...diffOpts.filter( - x => x !== 'check_all' && x !== 'check_sum', - ), - ]; - let rows = []; - directories.forEach(x => { - let row = []; - row.push(x.path); - columns.forEach(y => { - if (y !== '') { - y = y !== 'check_whodata' ? y : 'whodata'; - row.push(x[y] ? x[y] : 'no'); - } - }); - row.push(x.recursion_level); - rows.push(row); - }); - columns.forEach((x, idx) => { - columns[idx] = section.opts[x]; + const rows = obj.map(x => { + let row = []; + columns.forEach(key => { + row.push(x[key]); }); - columns.push('RL'); - tables.push({ - title: 'Monitored directories', - type: 'table', - columns, - rows, - }); - } else { + return row; + }); + columns.forEach((col, i) => { + columns[i] = col[0].toUpperCase() + col.slice(1); + }); + tables.push({ + title: 'Labels', + type: 'table', + columns, + rows, + }); + } else { + for (let _d2 of config.config[_d]) { tables.push( - ...this.getConfigTables( - context, - config.config[_d], - section, - idx, - ), + ...this.getConfigTables(context, _d2, section, idx), ); } } - for (const table of tables) { - printer.addConfigTables([table]); + } else { + /*INTEGRITY MONITORING MONITORED DIRECTORIES */ + if (config.config[_d].directories) { + const directories = config.config[_d].directories; + delete config.config[_d].directories; + tables.push( + ...this.getConfigTables( + context, + config.config[_d], + section, + idx, + ), + ); + let diffOpts = []; + Object.keys(section.opts).forEach(x => { + diffOpts.push(x); + }); + const columns = [ + '', + ...diffOpts.filter( + x => x !== 'check_all' && x !== 'check_sum', + ), + ]; + let rows = []; + directories.forEach(x => { + let row = []; + row.push(x.path); + columns.forEach(y => { + if (y !== '') { + y = y !== 'check_whodata' ? y : 'whodata'; + row.push(x[y] ? x[y] : 'no'); + } + }); + row.push(x.recursion_level); + rows.push(row); + }); + columns.forEach((x, idx) => { + columns[idx] = section.opts[x]; + }); + columns.push('RL'); + tables.push({ + title: 'Monitored directories', + type: 'table', + columns, + rows, + }); + } else { + tables.push( + ...this.getConfigTables( + context, + config.config[_d], + section, + idx, + ), + ); } - idx++; - tables = []; } + for (const table of tables) { + printer.addConfigTables([table]); + } + idx++; tables = []; } - } else { - printer.addContent({ - text: 'A configuration for this group has not yet been set up.', - style: { fontSize: 12, color: '#000' }, - margin: [0, 10, 0, 15], - }); + tables = []; } + } else { + printer.addContent({ + text: 'A configuration for this group has not yet been set up.', + style: { fontSize: 12, color: '#000' }, + margin: [0, 10, 0, 15], + }); } + } - // Agents in group - if (components['1']) { - await this.renderHeader( - context, - printer, - 'groupConfig', - groupID, - [], - apiId, - ); - } - - const buffer = await printer.print(); - - return response.ok({ - headers: { - 'Content-Type': 'application/pdf', - 'Content-Disposition': `attachment;filename="${context.wazuhEndpointParams.filename}"`, - 'Cache-Control': 'no-store,no-cache', - }, - body: buffer, - }); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse(error.message || error, 5029, 500, response); + // Agents in group + if (components['1']) { + await this.renderHeader( + context, + printer, + 'groupConfig', + groupID, + [], + apiId, + ); } - }, - ({ params: { groupID } }) => - `wazuh-group-configuration-${groupID}-${this.generateReportTimestamp()}.pdf`, - ); + + const buffer = await printer.print(); + const filename = `wazuh-group-configuration-${ + request.params.groupID + }-${this.generateReportTimestamp()}.pdf`; + + return response.ok({ + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment;filename="${filename}"`, + 'Cache-Control': 'no-store,no-cache', + }, + body: buffer, + }); + } catch (error) { + context.wazuh.logger.error(error.message || error); + return ErrorResponse(error.message || error, 5029, 500, response); + } + } /** * Create a report for the agents @@ -704,338 +682,300 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsAgentsConfiguration = - this.checkReportsUserDirectoryIsValidRouteDecorator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - context.wazuh.logger.debug('Report started'); - const { components, apiId } = request.body; - const { agentID } = request.params; + async createReportsAgentsConfiguration( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + ) { + try { + context.wazuh.logger.debug('Report started'); + const { components, apiId } = request.body; + const { agentID } = request.params; - const printer = new ReportPrinter( - context.wazuh.logger.get('report-printer'), - ); - createDataDirectoryIfNotExists(); - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH); - createDirectoryIfNotExists( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - ); - createDirectoryIfNotExists( - path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - context.wazuhEndpointParams.hashUsername, - ), - ); + const printer = new ReportPrinter( + context.wazuh.logger.get('report-printer'), + ); - let wmodulesResponse = {}; - let tables = []; - try { - wmodulesResponse = - await context.wazuh.api.client.asCurrentUser.request( - 'GET', - `/agents/${agentID}/config/wmodules/wmodules`, - {}, - { apiHostID: apiId }, - ); - } catch (error) { - context.wazuh.logger.debug(error.message || error); - } + let wmodulesResponse = {}; + let tables = []; + try { + wmodulesResponse = await context.wazuh.api.client.asCurrentUser.request( + 'GET', + `/agents/${agentID}/config/wmodules/wmodules`, + {}, + { apiHostID: apiId }, + ); + } catch (error) { + context.wazuh.logger.debug(error.message || error); + } - await this.renderHeader( - context, - printer, - 'agentConfig', - 'agentConfig', - agentID, - apiId, - ); + await this.renderHeader( + context, + printer, + 'agentConfig', + 'agentConfig', + agentID, + apiId, + ); - let idxComponent = 0; - for (let config of AgentConfiguration.configurations) { - let titleOfSection = false; + let idxComponent = 0; + for (let config of AgentConfiguration.configurations) { + let titleOfSection = false; + context.wazuh.logger.debug( + `Iterate over ${config.sections.length} configuration sections`, + ); + for (let section of config.sections) { + let titleOfSubsection = false; + if (components[idxComponent] && (section.config || section.wodle)) { + let idx = 0; + const configs = (section.config || []).concat(section.wodle || []); context.wazuh.logger.debug( - `Iterate over ${config.sections.length} configuration sections`, + `Iterate over ${configs.length} configuration blocks`, ); - for (let section of config.sections) { - let titleOfSubsection = false; - if ( - components[idxComponent] && - (section.config || section.wodle) - ) { - let idx = 0; - const configs = (section.config || []).concat( - section.wodle || [], - ); - context.wazuh.logger.debug( - `Iterate over ${configs.length} configuration blocks`, - ); - for (let conf of configs) { - let agentConfigResponse = {}; - try { - if (!conf['name']) { - agentConfigResponse = - await context.wazuh.api.client.asCurrentUser.request( - 'GET', - `/agents/${agentID}/config/${conf.component}/${conf.configuration}`, - {}, - { apiHostID: apiId }, - ); - } else { - for (let wodle of wmodulesResponse.data.data[ - 'wmodules' - ]) { - if (Object.keys(wodle)[0] === conf['name']) { - agentConfigResponse.data = { - data: wodle, - }; - } - } + for (let conf of configs) { + let agentConfigResponse = {}; + try { + if (!conf['name']) { + agentConfigResponse = + await context.wazuh.api.client.asCurrentUser.request( + 'GET', + `/agents/${agentID}/config/${conf.component}/${conf.configuration}`, + {}, + { apiHostID: apiId }, + ); + } else { + for (let wodle of wmodulesResponse.data.data['wmodules']) { + if (Object.keys(wodle)[0] === conf['name']) { + agentConfigResponse.data = { + data: wodle, + }; } + } + } - const agentConfig = - agentConfigResponse && - agentConfigResponse.data && - agentConfigResponse.data.data; - if (!titleOfSection) { - printer.addContent({ - text: config.title, - style: 'h1', - margin: [0, 0, 0, 15], - }); - titleOfSection = true; - } - if (!titleOfSubsection) { - printer.addContent({ - text: section.subtitle, - style: 'h4', - }); - printer.addContent({ - text: section.desc, - style: { fontSize: 12, color: '#000' }, - margin: [0, 0, 0, 10], - }); - titleOfSubsection = true; - } - if (agentConfig) { - for (let agentConfigKey of Object.keys(agentConfig)) { - if (Array.isArray(agentConfig[agentConfigKey])) { - /* LOG COLLECTOR */ - if (conf.filterBy) { - let groups = []; - agentConfig[agentConfigKey].forEach(obj => { - if (!groups[obj.logformat]) { - groups[obj.logformat] = []; - } - groups[obj.logformat].push(obj); - }); - Object.keys(groups).forEach(group => { - let saveidx = 0; - groups[group].forEach((x, i) => { - if ( - Object.keys(x).length > - Object.keys(groups[group][saveidx]).length - ) { - saveidx = i; - } - }); - const columns = Object.keys( - groups[group][saveidx], + const agentConfig = + agentConfigResponse && + agentConfigResponse.data && + agentConfigResponse.data.data; + if (!titleOfSection) { + printer.addContent({ + text: config.title, + style: 'h1', + margin: [0, 0, 0, 15], + }); + titleOfSection = true; + } + if (!titleOfSubsection) { + printer.addContent({ + text: section.subtitle, + style: 'h4', + }); + printer.addContent({ + text: section.desc, + style: { fontSize: 12, color: '#000' }, + margin: [0, 0, 0, 10], + }); + titleOfSubsection = true; + } + if (agentConfig) { + for (let agentConfigKey of Object.keys(agentConfig)) { + if (Array.isArray(agentConfig[agentConfigKey])) { + /* LOG COLLECTOR */ + if (conf.filterBy) { + let groups = []; + agentConfig[agentConfigKey].forEach(obj => { + if (!groups[obj.logformat]) { + groups[obj.logformat] = []; + } + groups[obj.logformat].push(obj); + }); + Object.keys(groups).forEach(group => { + let saveidx = 0; + groups[group].forEach((x, i) => { + if ( + Object.keys(x).length > + Object.keys(groups[group][saveidx]).length + ) { + saveidx = i; + } + }); + const columns = Object.keys(groups[group][saveidx]); + const rows = groups[group].map(x => { + let row = []; + columns.forEach(key => { + row.push( + typeof x[key] !== 'object' + ? x[key] + : Array.isArray(x[key]) + ? x[key].map(x => { + return x + '\n'; + }) + : JSON.stringify(x[key]), ); - const rows = groups[group].map(x => { - let row = []; - columns.forEach(key => { - row.push( - typeof x[key] !== 'object' - ? x[key] - : Array.isArray(x[key]) - ? x[key].map(x => { - return x + '\n'; - }) - : JSON.stringify(x[key]), - ); - }); - return row; - }); - columns.forEach((col, i) => { - columns[i] = - col[0].toUpperCase() + col.slice(1); - }); - tables.push({ - title: section.labels[0][group], - type: 'table', - columns, - rows, - }); }); - } else if ( - agentConfigKey.configuration !== 'socket' - ) { - tables.push( - ...this.getConfigTables( + return row; + }); + columns.forEach((col, i) => { + columns[i] = col[0].toUpperCase() + col.slice(1); + }); + tables.push({ + title: section.labels[0][group], + type: 'table', + columns, + rows, + }); + }); + } else if (agentConfigKey.configuration !== 'socket') { + tables.push( + ...this.getConfigTables( + context, + agentConfig[agentConfigKey], + section, + idx, + ), + ); + } else { + for (let _d2 of agentConfig[agentConfigKey]) { + tables.push( + ...this.getConfigTables(context, _d2, section, idx), + ); + } + } + } else { + /* INTEGRITY MONITORING MONITORED DIRECTORIES */ + if (conf.matrix) { + const { + directories, + diff, + synchronization, + file_limit, + ...rest + } = agentConfig[agentConfigKey]; + tables.push( + ...this.getConfigTables(context, rest, section, idx), + ...(diff && diff.disk_quota + ? this.getConfigTables( context, - agentConfig[agentConfigKey], - section, - idx, - ), - ); - } else { - for (let _d2 of agentConfig[agentConfigKey]) { - tables.push( - ...this.getConfigTables( - context, - _d2, - section, - idx, - ), - ); - } - } - } else { - /* INTEGRITY MONITORING MONITORED DIRECTORIES */ - if (conf.matrix) { - const { - directories, - diff, - synchronization, - file_limit, - ...rest - } = agentConfig[agentConfigKey]; - tables.push( - ...this.getConfigTables( + diff.disk_quota, + { tabs: ['Disk quota'] }, + 0, + ) + : []), + ...(diff && diff.file_size + ? this.getConfigTables( context, - rest, - section, - idx, - ), - ...(diff && diff.disk_quota - ? this.getConfigTables( - context, - diff.disk_quota, - { tabs: ['Disk quota'] }, - 0, - ) - : []), - ...(diff && diff.file_size - ? this.getConfigTables( - context, - diff.file_size, - { tabs: ['File size'] }, - 0, - ) - : []), - ...(synchronization - ? this.getConfigTables( - context, - synchronization, - { tabs: ['Synchronization'] }, - 0, - ) - : []), - ...(file_limit - ? this.getConfigTables( - context, - file_limit, - { tabs: ['File limit'] }, - 0, - ) - : []), - ); - let diffOpts = []; - Object.keys(section.opts).forEach(x => { - diffOpts.push(x); - }); - const columns = [ - '', - ...diffOpts.filter( - x => x !== 'check_all' && x !== 'check_sum', - ), - ]; - let rows = []; - directories.forEach(x => { - let row = []; - row.push(x.dir); - columns.forEach(y => { - if (y !== '') { - row.push( - x.opts.indexOf(y) > -1 ? 'yes' : 'no', - ); - } - }); - row.push(x.recursion_level); - rows.push(row); - }); - columns.forEach((x, idx) => { - columns[idx] = section.opts[x]; - }); - columns.push('RL'); - tables.push({ - title: 'Monitored directories', - type: 'table', - columns, - rows, - }); - } else { - tables.push( - ...this.getConfigTables( + diff.file_size, + { tabs: ['File size'] }, + 0, + ) + : []), + ...(synchronization + ? this.getConfigTables( context, - agentConfig[agentConfigKey], - section, - idx, - ), - ); - } - } + synchronization, + { tabs: ['Synchronization'] }, + 0, + ) + : []), + ...(file_limit + ? this.getConfigTables( + context, + file_limit, + { tabs: ['File limit'] }, + 0, + ) + : []), + ); + let diffOpts = []; + Object.keys(section.opts).forEach(x => { + diffOpts.push(x); + }); + const columns = [ + '', + ...diffOpts.filter( + x => x !== 'check_all' && x !== 'check_sum', + ), + ]; + let rows = []; + directories.forEach(x => { + let row = []; + row.push(x.dir); + columns.forEach(y => { + if (y !== '') { + row.push(x.opts.indexOf(y) > -1 ? 'yes' : 'no'); + } + }); + row.push(x.recursion_level); + rows.push(row); + }); + columns.forEach((x, idx) => { + columns[idx] = section.opts[x]; + }); + columns.push('RL'); + tables.push({ + title: 'Monitored directories', + type: 'table', + columns, + rows, + }); + } else { + tables.push( + ...this.getConfigTables( + context, + agentConfig[agentConfigKey], + section, + idx, + ), + ); } - } else { - // Print no configured module and link to the documentation - printer.addContent({ - text: [ - 'This module is not configured. Please take a look on how to configure it in ', - { - text: `${section.subtitle.toLowerCase()} configuration.`, - link: section.docuLink, - style: { fontSize: 12, color: '#1a0dab' }, - }, - ], - margin: [0, 0, 0, 20], - }); } - } catch (error) { - context.wazuh.logger.debug(error.message || error); } - idx++; - } - for (const table of tables) { - printer.addConfigTables([table]); + } else { + // Print no configured module and link to the documentation + printer.addContent({ + text: [ + 'This module is not configured. Please take a look on how to configure it in ', + { + text: `${section.subtitle.toLowerCase()} configuration.`, + link: section.docuLink, + style: { fontSize: 12, color: '#1a0dab' }, + }, + ], + margin: [0, 0, 0, 20], + }); } + } catch (error) { + context.wazuh.logger.debug(error.message || error); } - idxComponent++; - tables = []; + idx++; + } + for (const table of tables) { + printer.addConfigTables([table]); } } - - const buffer = await printer.print(); - - return response.ok({ - headers: { - 'Content-Type': 'application/pdf', - 'Content-Disposition': `attachment;filename="${context.wazuhEndpointParams.filename}"`, - 'Cache-Control': 'no-store,no-cache', - }, - body: buffer, - }); - } catch (error) { - context.wazuh.logger.debug(error.message || error); - return ErrorResponse(error.message || error, 5029, 500, response); + idxComponent++; + tables = []; } - }, - ({ params: { agentID } }) => - `wazuh-agent-configuration-${agentID}-${this.generateReportTimestamp()}.pdf`, - ); + } + + const buffer = await printer.print(); + const filename = `wazuh-agent-configuration-${ + request.params.agentID + }-${this.generateReportTimestamp()}.pdf`; + + return response.ok({ + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment;filename="${filename}"`, + 'Cache-Control': 'no-store,no-cache', + }, + body: buffer, + }); + } catch (error) { + context.wazuh.logger.debug(error.message || error); + return ErrorResponse(error.message || error, 5029, 500, response); + } + } /** * Create a report for the agents @@ -1044,340 +984,270 @@ export class WazuhReportingCtrl { * @param {Object} response * @returns {*} reports list or ErrorResponse */ - createReportsAgentsInventory = - this.checkReportsUserDirectoryIsValidRouteDecorator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - context.wazuh.logger.debug('Report started'); - const { - searchBar, - filters, - time, - indexPatternTitle, - apiId, - serverSideQuery, - } = request.body; - const { agentID } = request.params; - const { from, to } = time || {}; - // Init - const printer = new ReportPrinter( - context.wazuh.logger.get('report-printer'), - ); + async createReportsAgentsInventory( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + ) { + try { + context.wazuh.logger.debug('Report started'); + const { + searchBar, + filters, + time, + indexPatternTitle, + apiId, + serverSideQuery, + } = request.body; + const { agentID } = request.params; + const { from, to } = time || {}; + // Init + const printer = new ReportPrinter( + context.wazuh.logger.get('report-printer'), + ); - const { hashUsername } = await context.wazuh.security.getCurrentUser( - request, - context, - ); - createDataDirectoryIfNotExists(); - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH); - createDirectoryIfNotExists( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, + context.wazuh.logger.debug('Syscollector report'); + const [sanitizedFilters, agentsFilter] = filters + ? this.sanitizeKibanaFilters(context, filters, searchBar) + : [false, null]; + + // Get the agent OS + let agentOs = ''; + let isAgentWindows = false; + let isAgentLinux = false; + try { + const agentResponse = + await context.wazuh.api.client.asCurrentUser.request( + 'GET', + `/agents?agents_list=${agentID}`, + {}, + { apiHostID: apiId }, ); - createDirectoryIfNotExists( - path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - hashUsername, - ), + isAgentWindows = + agentResponse?.data?.data?.affected_items?.[0].os?.platform === + 'windows'; + isAgentLinux = + agentResponse?.data?.data?.affected_items?.[0].os?.uname?.includes( + 'Linux', ); + agentOs = + (isAgentWindows && 'windows') || (isAgentLinux && 'linux') || ''; + } catch (error) { + context.wazuh.logger.debug(error.message || error); + } - context.wazuh.logger.debug('Syscollector report'); - const [sanitizedFilters, agentsFilter] = filters - ? this.sanitizeKibanaFilters(context, filters, searchBar) - : [false, null]; - - // Get the agent OS - let agentOs = ''; - let isAgentWindows = false; - let isAgentLinux = false; - try { - const agentResponse = - await context.wazuh.api.client.asCurrentUser.request( - 'GET', - `/agents?agents_list=${agentID}`, - {}, - { apiHostID: apiId }, - ); - isAgentWindows = - agentResponse?.data?.data?.affected_items?.[0].os?.platform === - 'windows'; - isAgentLinux = - agentResponse?.data?.data?.affected_items?.[0].os?.uname?.includes( - 'Linux', - ); - agentOs = - (isAgentWindows && 'windows') || (isAgentLinux && 'linux') || ''; - } catch (error) { - context.wazuh.logger.debug(error.message || error); - } - - // Add title - printer.addContentWithNewLine({ - text: 'Inventory data report', - style: 'h1', - }); - - // Add table with the agent info - await buildAgentsTable(context, printer, [agentID], apiId); - - // Get syscollector packages and processes - const agentRequestsInventory = [ - { - endpoint: `/syscollector/${agentID}/packages`, - loggerMessage: `Fetching packages for agent ${agentID}`, - table: { - title: 'Packages', - columns: - agentOs === 'windows' - ? [ - { id: 'name', label: 'Name' }, - { id: 'architecture', label: 'Architecture' }, - { id: 'version', label: 'Version' }, - { id: 'vendor', label: 'Vendor' }, - ] - : [ - { id: 'name', label: 'Name' }, - { id: 'architecture', label: 'Architecture' }, - { id: 'version', label: 'Version' }, - { id: 'vendor', label: 'Vendor' }, - { id: 'description', label: 'Description' }, - ], - }, - }, - { - endpoint: `/syscollector/${agentID}/processes`, - loggerMessage: `Fetching processes for agent ${agentID}`, - table: { - title: 'Processes', - columns: - agentOs === 'windows' - ? [ - { id: 'name', label: 'Name' }, - { id: 'cmd', label: 'CMD' }, - { id: 'priority', label: 'Priority' }, - { id: 'nlwp', label: 'NLWP' }, - ] - : [ - { id: 'name', label: 'Name' }, - { id: 'euser', label: 'Effective user' }, - { id: 'nice', label: 'Priority' }, - { id: 'state', label: 'State' }, - ], - }, - mapResponseItems: item => - agentOs === 'windows' - ? item - : { ...item, state: ProcessEquivalence[item.state] }, - }, - { - endpoint: `/syscollector/${agentID}/ports`, - loggerMessage: `Fetching ports for agent ${agentID}`, - table: { - title: 'Network ports', - columns: - agentOs === 'windows' - ? [ - { id: 'local_port', label: 'Local port' }, - { id: 'local_ip', label: 'Local IP address' }, - { id: 'process', label: 'Process' }, - { id: 'state', label: 'State' }, - { id: 'protocol', label: 'Protocol' }, - ] - : agentOs === 'linux' - ? [ - { id: 'local_port', label: 'Local port' }, - { id: 'local_ip', label: 'Local IP address' }, - { id: 'process', label: 'Process' }, - { id: 'pid', label: 'PID' }, - { id: 'state', label: 'State' }, - { id: 'protocol', label: 'Protocol' }, - ] - : [ - { id: 'local_port', label: 'Local port' }, - { id: 'local_ip', label: 'Local IP address' }, - { id: 'state', label: 'State' }, - { id: 'protocol', label: 'Protocol' }, - ], - }, - mapResponseItems: item => ({ - ...item, - local_ip: item.local.ip, - local_port: item.local.port, - }), - }, - { - endpoint: `/syscollector/${agentID}/netiface`, - loggerMessage: `Fetching netiface for agent ${agentID}`, - table: { - title: 'Network interfaces', - columns: [ - { id: 'name', label: 'Name' }, - { id: 'mac', label: 'Mac' }, - { id: 'state', label: 'State' }, - { id: 'mtu', label: 'MTU' }, - { id: 'type', label: 'Type' }, - ], - }, - }, - { - endpoint: `/syscollector/${agentID}/netaddr`, - loggerMessage: `Fetching netaddr for agent ${agentID}`, - table: { - title: 'Network settings', - columns: [ - { id: 'iface', label: 'Interface' }, - { id: 'address', label: 'Address' }, - { id: 'netmask', label: 'Netmask' }, - { id: 'proto', label: 'Protocol' }, - { id: 'broadcast', label: 'Broadcast' }, - ], - }, - }, - ]; - - agentOs === 'windows' && - agentRequestsInventory.push({ - endpoint: `/syscollector/${agentID}/hotfixes`, - loggerMessage: `Fetching hotfixes for agent ${agentID}`, - table: { - title: 'Windows updates', - columns: [{ id: 'hotfix', label: 'Update code' }], - }, - }); - - const requestInventory = async agentRequestInventory => { - try { - context.wazuh.logger.debug(agentRequestInventory.loggerMessage); - - const inventoryResponse = - await context.wazuh.api.client.asCurrentUser.request( - 'GET', - agentRequestInventory.endpoint, - {}, - { apiHostID: apiId }, - ); - - const inventory = - inventoryResponse && - inventoryResponse.data && - inventoryResponse.data.data && - inventoryResponse.data.data.affected_items; - if (inventory) { - return { - ...agentRequestInventory.table, - items: agentRequestInventory.mapResponseItems - ? inventory.map(agentRequestInventory.mapResponseItems) - : inventory, - }; - } - } catch (error) { - context.wazuh.logger.debug(error.message || error); - } - }; + // Add title + printer.addContentWithNewLine({ + text: 'Inventory data report', + style: 'h1', + }); - if (time) { - // Add Vulnerability Detector filter to the Server Side Query - serverSideQuery?.bool?.must?.push?.({ - match_phrase: { - 'rule.groups': { - query: 'vulnerability-detector', - }, - }, - }); + // Add table with the agent info + await buildAgentsTable(context, printer, [agentID], apiId); + + // Get syscollector packages and processes + const agentRequestsInventory = [ + { + endpoint: `/syscollector/${agentID}/packages`, + loggerMessage: `Fetching packages for agent ${agentID}`, + table: { + title: 'Packages', + columns: + agentOs === 'windows' + ? [ + { id: 'name', label: 'Name' }, + { id: 'architecture', label: 'Architecture' }, + { id: 'version', label: 'Version' }, + { id: 'vendor', label: 'Vendor' }, + ] + : [ + { id: 'name', label: 'Name' }, + { id: 'architecture', label: 'Architecture' }, + { id: 'version', label: 'Version' }, + { id: 'vendor', label: 'Vendor' }, + { id: 'description', label: 'Description' }, + ], + }, + }, + { + endpoint: `/syscollector/${agentID}/processes`, + loggerMessage: `Fetching processes for agent ${agentID}`, + table: { + title: 'Processes', + columns: + agentOs === 'windows' + ? [ + { id: 'name', label: 'Name' }, + { id: 'cmd', label: 'CMD' }, + { id: 'priority', label: 'Priority' }, + { id: 'nlwp', label: 'NLWP' }, + ] + : [ + { id: 'name', label: 'Name' }, + { id: 'euser', label: 'Effective user' }, + { id: 'nice', label: 'Priority' }, + { id: 'state', label: 'State' }, + ], + }, + mapResponseItems: item => + agentOs === 'windows' + ? item + : { ...item, state: ProcessEquivalence[item.state] }, + }, + { + endpoint: `/syscollector/${agentID}/ports`, + loggerMessage: `Fetching ports for agent ${agentID}`, + table: { + title: 'Network ports', + columns: + agentOs === 'windows' + ? [ + { id: 'local_port', label: 'Local port' }, + { id: 'local_ip', label: 'Local IP address' }, + { id: 'process', label: 'Process' }, + { id: 'state', label: 'State' }, + { id: 'protocol', label: 'Protocol' }, + ] + : agentOs === 'linux' + ? [ + { id: 'local_port', label: 'Local port' }, + { id: 'local_ip', label: 'Local IP address' }, + { id: 'process', label: 'Process' }, + { id: 'pid', label: 'PID' }, + { id: 'state', label: 'State' }, + { id: 'protocol', label: 'Protocol' }, + ] + : [ + { id: 'local_port', label: 'Local port' }, + { id: 'local_ip', label: 'Local IP address' }, + { id: 'state', label: 'State' }, + { id: 'protocol', label: 'Protocol' }, + ], + }, + mapResponseItems: item => ({ + ...item, + local_ip: item.local.ip, + local_port: item.local.port, + }), + }, + { + endpoint: `/syscollector/${agentID}/netiface`, + loggerMessage: `Fetching netiface for agent ${agentID}`, + table: { + title: 'Network interfaces', + columns: [ + { id: 'name', label: 'Name' }, + { id: 'mac', label: 'Mac' }, + { id: 'state', label: 'State' }, + { id: 'mtu', label: 'MTU' }, + { id: 'type', label: 'Type' }, + ], + }, + }, + { + endpoint: `/syscollector/${agentID}/netaddr`, + loggerMessage: `Fetching netaddr for agent ${agentID}`, + table: { + title: 'Network settings', + columns: [ + { id: 'iface', label: 'Interface' }, + { id: 'address', label: 'Address' }, + { id: 'netmask', label: 'Netmask' }, + { id: 'proto', label: 'Protocol' }, + { id: 'broadcast', label: 'Broadcast' }, + ], + }, + }, + ]; + + agentOs === 'windows' && + agentRequestsInventory.push({ + endpoint: `/syscollector/${agentID}/hotfixes`, + loggerMessage: `Fetching hotfixes for agent ${agentID}`, + table: { + title: 'Windows updates', + columns: [{ id: 'hotfix', label: 'Update code' }], + }, + }); - await extendedInformation( - context, - printer, - 'agents', - 'syscollector', - apiId, - from, - to, - serverSideQuery, - agentsFilter, - indexPatternTitle, - agentID, + const requestInventory = async agentRequestInventory => { + try { + context.wazuh.logger.debug(agentRequestInventory.loggerMessage); + + const inventoryResponse = + await context.wazuh.api.client.asCurrentUser.request( + 'GET', + agentRequestInventory.endpoint, + {}, + { apiHostID: apiId }, ); - } - - // Add inventory tables - (await Promise.all(agentRequestsInventory.map(requestInventory))) - .filter(table => table) - .forEach(table => printer.addSimpleTable(table)); - // Print the document - const buffer = await printer.print(); - - return response.ok({ - headers: { - 'Content-Type': 'application/pdf', - 'Content-Disposition': `attachment;filename="${context.wazuhEndpointParams.filename}"`, - 'Cache-Control': 'no-store,no-cache', - }, - body: buffer, - }); + const inventory = + inventoryResponse && + inventoryResponse.data && + inventoryResponse.data.data && + inventoryResponse.data.data.affected_items; + if (inventory) { + return { + ...agentRequestInventory.table, + items: agentRequestInventory.mapResponseItems + ? inventory.map(agentRequestInventory.mapResponseItems) + : inventory, + }; + } } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse(error.message || error, 5029, 500, response); + context.wazuh.logger.debug(error.message || error); } - }, - ({ params: { agentID } }) => - `wazuh-agent-inventory-${agentID}-${this.generateReportTimestamp()}.pdf`, - ); - - checkReportsUserDirectoryIsValidRouteDecorator( - routeHandler, - reportFileNameAccessor, - ) { - return async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - const { username, hashUsername } = - await context.wazuh.security.getCurrentUser(request, context); - const userReportsDirectoryPath = path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - hashUsername, - ); - const filename = reportFileNameAccessor(request); - const pathFilename = path.join(userReportsDirectoryPath, filename); - context.wazuh.logger.debug( - `Checking the user ${username}(${hashUsername}) can do actions in the reports file: ${pathFilename}`, - ); - if ( - !pathFilename.startsWith(userReportsDirectoryPath) || - pathFilename.includes('../') - ) { - context.wazuh.logger.warn( - `User ${username}(${hashUsername}) tried to access to a non user report file: ${pathFilename}`, - ); - return response.badRequest({ - body: { - message: '5040 - You shall not pass!', + }; + + if (time) { + // Add Vulnerability Detector filter to the Server Side Query + serverSideQuery?.bool?.must?.push?.({ + match_phrase: { + 'rule.groups': { + query: 'vulnerability-detector', }, - }); - } - context.wazuh.logger.debug( - 'Checking the user can do actions in the reports file', - ); - return await routeHandler.bind(this)( - { - ...context, - wazuhEndpointParams: { hashUsername, filename, pathFilename }, }, - request, - response, + }); + + await extendedInformation( + context, + printer, + 'agents', + 'syscollector', + apiId, + from, + to, + serverSideQuery, + agentsFilter, + indexPatternTitle, + agentID, ); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse(error.message || error, 5040, 500, response); } - }; + + // Add inventory tables + (await Promise.all(agentRequestsInventory.map(requestInventory))) + .filter(table => table) + .forEach(table => printer.addSimpleTable(table)); + + // Print the document + const buffer = await printer.print(); + const filename = `wazuh-agent-inventory-${ + request.params.agentID + }-${this.generateReportTimestamp()}.pdf`; + + return response.ok({ + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment;filename="${filename}"`, + 'Cache-Control': 'no-store,no-cache', + }, + body: buffer, + }); + } catch (error) { + context.wazuh.logger.error(error.message || error); + return ErrorResponse(error.message || error, 5029, 500, response); + } } private generateReportTimestamp() { From e9fee73537bbb3d44dce2cc6204646d378066590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 8 Mar 2024 11:39:48 +0100 Subject: [PATCH 06/12] fix(reporting): fix filename of agents modules PDF report --- plugins/main/server/controllers/wazuh-reporting.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-reporting.ts b/plugins/main/server/controllers/wazuh-reporting.ts index e2cc6b8ee3..33855d7c1c 100644 --- a/plugins/main/server/controllers/wazuh-reporting.ts +++ b/plugins/main/server/controllers/wazuh-reporting.ts @@ -374,9 +374,7 @@ export class WazuhReportingCtrl { const buffer = await printer.print(); const filename = `wazuh-module-${ - request.body.agents - ? `agents-${request.body.agents.agents}` - : 'overview' + request.body.agents ? `agents-${request.body.agents}` : 'overview' }-${request.params.moduleID}-${this.generateReportTimestamp()}.pdf`; return response.ok({ From 655d4e7a185848b5277ea9dc6ac7adde934640d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 8 Mar 2024 17:02:40 +0100 Subject: [PATCH 07/12] fix(reporting): remove unused imports --- plugins/main/server/controllers/wazuh-reporting.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-reporting.ts b/plugins/main/server/controllers/wazuh-reporting.ts index 33855d7c1c..36b2741723 100644 --- a/plugins/main/server/controllers/wazuh-reporting.ts +++ b/plugins/main/server/controllers/wazuh-reporting.ts @@ -9,7 +9,6 @@ * * Find more information about this on the LICENSE file. */ -import path from 'path'; import { WAZUH_MODULES } from '../../common/wazuh-modules'; import { ErrorResponse } from '../lib/error-response'; import ProcessEquivalence from '../lib/process-state-equivalence'; @@ -26,15 +25,9 @@ import { } from '../lib/reporting/extended-information'; import { ReportPrinter } from '../lib/reporting/printer'; import { - WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, AUTHORIZED_AGENTS, API_NAME_AGENT_STATUS, } from '../../common/constants'; -import { - createDirectoryIfNotExists, - createDataDirectoryIfNotExists, -} from '../lib/filesystem'; import { agentStatusLabelByAgentStatus } from '../../common/services/wz_agent_status'; interface AgentsFilter { From 1241c001f3cc5cabd6dede97f7e4091e73b72908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 11 Mar 2024 09:41:53 +0100 Subject: [PATCH 08/12] feat(reporting): replace taking screenshot of visualuzations - Replace taking screenshot of visualuzations in startVis2Png method - Communicate dashboard with the Generate report button though Redux - Create reducer - Create action creator - Adapt the Generate report button - Remove disabledReport property comming from old dashboard rendering - Refactor the method to add the visualizations to the PDF report in the Printer service --- ...se-reporting-communicate-search-context.ts | 16 ++ .../modules/buttons/generate_report.tsx | 96 ++++++----- .../public/controllers/overview/overview.js | 4 +- .../main/public/react-services/reporting.js | 72 ++++---- .../public/redux/actions/reportingActions.js | 20 ++- .../redux/reducers/reportingReducers.js | 18 +- plugins/main/server/lib/reporting/printer.ts | 158 +++++++++++------- 7 files changed, 228 insertions(+), 156 deletions(-) create mode 100644 plugins/main/public/components/common/hooks/use-reporting-communicate-search-context.ts diff --git a/plugins/main/public/components/common/hooks/use-reporting-communicate-search-context.ts b/plugins/main/public/components/common/hooks/use-reporting-communicate-search-context.ts new file mode 100644 index 0000000000..d435605769 --- /dev/null +++ b/plugins/main/public/components/common/hooks/use-reporting-communicate-search-context.ts @@ -0,0 +1,16 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { updateReportingCommunicateSearchContext } from '../../../redux/actions/reportingActions'; + +/** + * WORKAROUND: this hook stores the search context to be used by the Generate report button of + * module dashboards + * @param context + */ +export function useReportingCommunicateSearchContext(context) { + const dispatch = useDispatch(); + useEffect(() => { + dispatch(updateReportingCommunicateSearchContext(context)); + return () => dispatch(updateReportingCommunicateSearchContext(null)); + }, [JSON.stringify(context)]); +} diff --git a/plugins/main/public/components/common/modules/buttons/generate_report.tsx b/plugins/main/public/components/common/modules/buttons/generate_report.tsx index 155963d8cb..c3f17a225a 100644 --- a/plugins/main/public/components/common/modules/buttons/generate_report.tsx +++ b/plugins/main/public/components/common/modules/buttons/generate_report.tsx @@ -16,48 +16,66 @@ import { getUiSettings } from '../../../../kibana-services'; import { ReportingService } from '../../../../react-services'; import $ from 'jquery'; import { WzButton } from '../../../common/buttons'; +import { connect } from 'react-redux'; +const mapStateToProps = state => ({ + dataSourceSearchContext: state.reportingReducers.dataSourceSearchContext, +}); -export const ButtonModuleGenerateReport = ({agent, moduleID, disabledReport}) => { - const action = useAsyncAction(async () => { - const reportingService = new ReportingService(); - const isDarkModeTheme = getUiSettings().get('theme:darkMode'); - if (isDarkModeTheme) { +export const ButtonModuleGenerateReport = connect(mapStateToProps)( + ({ agent, moduleID, dataSourceSearchContext }) => { + const disabledReport = ![ + !dataSourceSearchContext?.isLoading, + !dataSourceSearchContext?.isSearching, + dataSourceSearchContext?.totalResults, + dataSourceSearchContext?.indexPattern, + ].every(value => Boolean(value)); + const totalResults = dataSourceSearchContext?.totalResults; + const action = useAsyncAction(async () => { + const reportingService = new ReportingService(); + const isDarkModeTheme = getUiSettings().get('theme:darkMode'); + if (isDarkModeTheme) { + //Patch to fix white text in dark-mode pdf reports + const defaultTextColor = '#DFE5EF'; - //Patch to fix white text in dark-mode pdf reports - const defaultTextColor = '#DFE5EF'; + //Patch to fix dark backgrounds in visualizations dark-mode pdf reports + const $labels = $('.euiButtonEmpty__text, .echLegendItem'); + const $vizBackground = $('.echChartBackground'); + const defaultVizBackground = $vizBackground.css('background-color'); - //Patch to fix dark backgrounds in visualizations dark-mode pdf reports - const $labels = $('.euiButtonEmpty__text, .echLegendItem'); - const $vizBackground = $('.echChartBackground'); - const defaultVizBackground = $vizBackground.css('background-color'); - - try { - $labels.css('color', 'black'); - $vizBackground.css('background-color', 'transparent'); - await reportingService.startVis2Png(moduleID, agent?.id || false) - $vizBackground.css('background-color', defaultVizBackground); - $labels.css('color', defaultTextColor); - } catch (e) { - $labels.css('color', defaultTextColor); - $vizBackground.css('background-color', defaultVizBackground); + try { + $labels.css('color', 'black'); + $vizBackground.css('background-color', 'transparent'); + await reportingService.startVis2Png(moduleID, agent?.id || false); + $vizBackground.css('background-color', defaultVizBackground); + $labels.css('color', defaultTextColor); + } catch (e) { + $labels.css('color', defaultTextColor); + $vizBackground.css('background-color', defaultVizBackground); + } + } else { + await reportingService.startVis2Png(moduleID, agent?.id || false); } - } else { - await reportingService.startVis2Png(moduleID, agent?.id || false) - } - }, [agent]); - - return ( - - Generate report - - ) -} + }, [agent]); + return ( + + Generate report + + ); + }, +); diff --git a/plugins/main/public/controllers/overview/overview.js b/plugins/main/public/controllers/overview/overview.js index 6b8a2ee765..38936fa3e5 100644 --- a/plugins/main/public/controllers/overview/overview.js +++ b/plugins/main/public/controllers/overview/overview.js @@ -76,7 +76,7 @@ export class OverviewController { this.currentOverviewSectionProps = { switchTab: (tab, force) => this.switchTab(tab, force), - currentTab: this.tab + currentTab: this.tab, }; } @@ -95,13 +95,11 @@ export class OverviewController { this.wzMonitoringEnabled = false; - this.init(); this.$scope.getMainProps = resultState => { return { section: this.tab, - disabledReport: resultState !== 'ready', agentsSelectionProps: this.agentsSelectionProps, switchSubTab: subtab => this.switchSubtab(subtab), }; diff --git a/plugins/main/public/react-services/reporting.js b/plugins/main/public/react-services/reporting.js index 8f1612a5e8..0db20a9d8e 100644 --- a/plugins/main/public/react-services/reporting.js +++ b/plugins/main/public/react-services/reporting.js @@ -23,6 +23,8 @@ import { UI_LOGGER_LEVELS } from '../../common/constants'; import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; import { getErrorOrchestrator } from './common-services'; import { exportResponseToFile } from './export-file'; +import domtoimage from '../utils/dom-to-image'; +import store from '../redux/store'; const app = getAngularModule(); @@ -61,63 +63,49 @@ export class ReportingService { return idArray; } + async getVisualizationsFromDOM() { + const domVisualizations = document.querySelectorAll('.visualization'); + return await Promise.all( + Array.from(domVisualizations).map(async node => { + return { + element: await domtoimage.toPng(node), + width: node.clientWidth, + height: node.clientHeight, + title: node?.parentNode?.parentNode?.parentNode?.querySelector( + 'figcaption > h2 > .embPanel__titleInner', + )?.textContent, + }; + }), + ); + } + + async getDataSourceSearchContext() { + return store.getState().reportingReducers?.dataSourceSearchContext; + } + async startVis2Png(tab, agents = false, syscollectorFilters = null) { try { - if (this.vis2png.isWorking()) { - this.showToast('danger', 'Error', 'Report in progress', 4000); - return; - } this.$rootScope.reportBusy = true; this.$rootScope.reportStatus = 'Generating report...0%'; this.$rootScope.$applyAsync(); - - this.vis2png.clear(); - - const rawVisualizations = this.rawVisualizations - .getList() - .filter(this.removeTableVis); - - let idArray = []; - if (tab === 'general') { - idArray = this.removeAgentStatusVis( - rawVisualizations.map(item => item.id), - ); - } else { - idArray = rawVisualizations.map(item => item.id); - } - - const visualizationIDList = []; - for (const item of idArray) { - const tmpHTMLElement = $(`#${item}`); - if (tmpHTMLElement[0]) { - this.vis2png.assignHTMLItem(item, tmpHTMLElement); - visualizationIDList.push(item); - } - } - - const appliedFilters = await this.visHandlers.getAppliedFilters( - syscollectorFilters, - ); + const dataSourceContext = await this.getDataSourceSearchContext(); + const visualizations = await this.getVisualizationsFromDOM(); const dataplugin = await getDataPlugin(); const serverSideQuery = dataplugin.query.getOpenSearchQuery(); - const array = await this.vis2png.checkArray(visualizationIDList); - const browserTimezone = moment.tz.guess(true); const data = { - array, + array: visualizations, serverSideQuery, // Used for applying the same filters on the server side requests - filters: appliedFilters.filters, - time: appliedFilters.time, - searchBar: appliedFilters.searchBar, - tables: appliedFilters.tables, + filters: dataSourceContext.filters, + time: dataSourceContext.time, + searchBar: dataSourceContext.query?.query || '', + tables: [], // TODO: check is this is used tab, section: agents ? 'agents' : 'overview', agents, browserTimezone, - indexPatternTitle: ( - await getDataPlugin().indexPatterns.get(AppState.getCurrentPattern()) - ).title, + indexPatternTitle: dataSourceContext.indexPattern.title, apiId: JSON.parse(AppState.getCurrentAPI()).id, }; diff --git a/plugins/main/public/redux/actions/reportingActions.js b/plugins/main/public/redux/actions/reportingActions.js index 9ada6e18e3..a6dfd76ad3 100644 --- a/plugins/main/public/redux/actions/reportingActions.js +++ b/plugins/main/public/redux/actions/reportingActions.js @@ -17,7 +17,7 @@ export const updateIsProcessing = isProcessing => { return { type: 'UPDATE_IS_PROCESSING', - isProcessing: isProcessing + isProcessing: isProcessing, }; }; @@ -28,7 +28,7 @@ export const updateIsProcessing = isProcessing => { export const updateListItemsForRemove = itemList => { return { type: 'UPDATE_LIST_ITEMS_FOR_REMOVE', - itemList: itemList + itemList: itemList, }; }; @@ -39,7 +39,7 @@ export const updateListItemsForRemove = itemList => { export const updateShowModal = showModal => { return { type: 'UPDATE_SHOW_MODAL', - showModal: showModal + showModal: showModal, }; }; @@ -49,6 +49,18 @@ export const updateShowModal = showModal => { */ export const cleanInfo = () => { return { - type: 'CLEAN_INFO' + type: 'CLEAN_INFO', }; }; + +/** + * Update reporting data source search context + * @param {Object} dataSourceSearchContext + */ +export const updateReportingCommunicateSearchContext = + dataSourceSearchContext => { + return { + type: 'UPDATE_REPORTING_DATA_SEARCH_SOURCE_CONTEXT', + dataSourceSearchContext, + }; + }; diff --git a/plugins/main/public/redux/reducers/reportingReducers.js b/plugins/main/public/redux/reducers/reportingReducers.js index 5e53baa9b2..063b11a7b8 100644 --- a/plugins/main/public/redux/reducers/reportingReducers.js +++ b/plugins/main/public/redux/reducers/reportingReducers.js @@ -9,12 +9,14 @@ * * Find more information about this on the LICENSE file. */ +import { cloneDeep } from 'lodash'; const initialState = { isLoading: false, isProcessing: false, itemList: [], - showModal: false + showModal: false, + dataSourceSearchContext: null, }; const statusReducers = (state = initialState, action) => { @@ -22,19 +24,19 @@ const statusReducers = (state = initialState, action) => { return { ...state, isProcessing: action.isProcessing, - isLoading: action.isProcessing + isLoading: action.isProcessing, }; } if (action.type === 'UPDATE_LIST_ITEMS_FOR_REMOVE') { return { ...state, - itemList: action.itemList + itemList: action.itemList, }; } if (action.type === 'UPDATE_SHOW_MODAL') { return { ...state, - showModal: action.showModal + showModal: action.showModal, }; } if (action.type === 'CLEAN_INFO') { @@ -43,7 +45,13 @@ const statusReducers = (state = initialState, action) => { isLoading: false, isProcessing: false, itemList: [], - showModal: false + showModal: false, + }; + } + if (action.type === 'UPDATE_REPORTING_DATA_SEARCH_SOURCE_CONTEXT') { + return { + ...state, + dataSourceSearchContext: cloneDeep(action.dataSourceSearchContext), }; } diff --git a/plugins/main/server/lib/reporting/printer.ts b/plugins/main/server/lib/reporting/printer.ts index 13038a5a62..329915b6e5 100644 --- a/plugins/main/server/lib/reporting/printer.ts +++ b/plugins/main/server/lib/reporting/printer.ts @@ -13,6 +13,17 @@ import { REPORTS_PRIMARY_COLOR } from '../../../common/constants'; import { getCustomizationSetting } from '../../../common/services/settings'; import { Logger } from 'opensearch-dashboards/server'; +interface IVisualization { + title: string; + element: string; + height: number; + width?: number; +} + +type IVisualizationExtended = IVisualization & { + id: string; + width: number; +}; const COLORS = { PRIMARY: REPORTS_PRIMARY_COLOR, }; @@ -354,77 +365,98 @@ export class ReportPrinter { this.addContent({ text: '\n' }); this.logger.debug('Time range and filters rendered'); } - addVisualizations(visualizations, isAgents, tab) { - this.logger.debug(`${visualizations.length} visualizations for tab ${tab}`); - const single_vis = visualizations.filter(item => item.width >= 600); - const double_vis = visualizations.filter(item => item.width < 600); - - single_vis.forEach(visualization => { - const title = this.checkTitle(visualization, isAgents, tab); - this.addContent({ - id: 'singlevis' + title[0]._source.title, - text: title[0]._source.title, + private addVisualizationSingle(visualization: IVisualizationExtended) { + this.addContent({ + id: 'singlevis' + visualization.id, + text: visualization.title, + style: 'h3', + }); + this.addContent({ + columns: [{ image: visualization.element, width: 500 }], + }); + this.addNewLine(); + } + private addVisualizationSplit( + split: [IVisualizationExtended, IVisualizationExtended], + ) { + this.addContent({ + columns: split.map(visualization => ({ + id: 'splitvis' + visualization.id, + text: visualization.title, style: 'h3', - }); - this.addContent({ - columns: [{ image: visualization.element, width: 500 }], - }); - this.addNewLine(); + width: 280, + })), }); - let pair = []; + this.addContent({ + columns: split.map(visualization => ({ + image: visualization.element, + width: 270, + })), + }); - for (const item of double_vis) { - pair.push(item); - if (pair.length === 2) { - const title_1 = this.checkTitle(pair[0], isAgents, tab); - const title_2 = this.checkTitle(pair[1], isAgents, tab); + this.addNewLine(); + } + private addVisualizationSplitSingle(visualization: IVisualizationExtended) { + this.addContent({ + columns: [ + { + id: 'splitsinglevis' + visualization.id, + text: visualization.title, + style: 'h3', + width: 280, + }, + ], + }); + this.addContent({ + columns: [{ image: visualization.element, width: 280 }], + }); + this.addNewLine(); + } + addVisualizations(visualizations: IVisualization[]) { + this.logger.debug(`Add visualizations [${visualizations.length}]`); + const sanitazedVisualizations: IVisualizationExtended[] = + visualizations.map((visualization, index) => ({ + ...visualization, + title: visualization.title || '', + id: `${visualization.title || ''}.${index}`, + })); + const { single: fullWidthVisualizations, split: splitWidthVisualizations } = + sanitazedVisualizations.reduce( + (accum, visualization) => { + ( + (visualization.width >= 600 + ? accum.single + : accum.split) as IVisualizationExtended[] + ).push(visualization); + return accum; + }, + { single: [], split: [] }, + ); - this.addContent({ - columns: [ - { - id: 'splitvis' + title_1[0]._source.title, - text: title_1[0]._source.title, - style: 'h3', - width: 280, - }, - { - id: 'splitvis' + title_2[0]._source.title, - text: title_2[0]._source.title, - style: 'h3', - width: 280, - }, - ], - }); + fullWidthVisualizations.forEach(visualization => + this.addVisualizationSingle(visualization), + ); - this.addContent({ - columns: [ - { image: pair[0].element, width: 270 }, - { image: pair[1].element, width: 270 }, - ], - }); + const splitBy = 2; + const splits = splitWidthVisualizations.reduce(function ( + accum, + value, + index, + array, + ) { + if (index % splitBy === 0) + accum.push(array.slice(index, index + splitBy)); + return accum; + }, + []); - this.addNewLine(); - pair = []; + splits.forEach(split => { + if (split.length === splitBy) { + return this.addVisualizationSplit(split); } - } - - if (double_vis.length % 2 !== 0) { - const item = double_vis[double_vis.length - 1]; - const title = this.checkTitle(item, isAgents, tab); - this.addContent({ - columns: [ - { - id: 'splitsinglevis' + title[0]._source.title, - text: title[0]._source.title, - style: 'h3', - width: 280, - }, - ], - }); - this.addContent({ columns: [{ image: item.element, width: 280 }] }); - this.addNewLine(); - } + this.addVisualizationSplitSingle(split[0]); + }); } formatDate(date: Date): string { this.logger.debug(`Format date ${date}`); From 4a780b811ef3f7a59e7392374cec95cc31bbb2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 11 Mar 2024 10:50:02 +0100 Subject: [PATCH 09/12] feat(reporting): remove unused reporting service (AngularJS) --- .../main/public/controllers/agent/agents.js | 11 -- .../public/controllers/overview/overview.js | 10 - plugins/main/public/services/index.js | 2 - plugins/main/public/services/reporting.js | 173 ------------------ 4 files changed, 196 deletions(-) delete mode 100644 plugins/main/public/services/reporting.js diff --git a/plugins/main/public/controllers/agent/agents.js b/plugins/main/public/controllers/agent/agents.js index 1daf8826fd..dfc841555a 100644 --- a/plugins/main/public/controllers/agent/agents.js +++ b/plugins/main/public/controllers/agent/agents.js @@ -38,7 +38,6 @@ export class AgentsController { * @param {Object} $rootScope * @param {Object} errorHandler * @param {Object} commonData - * @param {Object} reportingService * @param {Object} visFactoryService * @param {Object} csvReq */ @@ -48,7 +47,6 @@ export class AgentsController { $rootScope, errorHandler, commonData, - reportingService, visFactoryService, csvReq, ) { @@ -60,7 +58,6 @@ export class AgentsController { this.$scope.visualizations = visualizations; this.shareAgent = new ShareAgent(); this.commonData = commonData; - this.reportingService = reportingService; this.visFactoryService = visFactoryService; this.csvReq = csvReq; this.wazuhConfig = new WazuhConfig(); @@ -189,14 +186,6 @@ export class AgentsController { this.$location.path('/manager/groups'); }; - this.$scope.exportConfiguration = enabledComponents => { - this.reportingService.startConfigReport( - this.$scope.agent, - 'agentConfig', - enabledComponents, - ); - }; - //Load try { this.$scope.getAgent(); diff --git a/plugins/main/public/controllers/overview/overview.js b/plugins/main/public/controllers/overview/overview.js index 38936fa3e5..ceee10d8c8 100644 --- a/plugins/main/public/controllers/overview/overview.js +++ b/plugins/main/public/controllers/overview/overview.js @@ -39,7 +39,6 @@ export class OverviewController { * @param {*} $rootScope * @param {*} errorHandler * @param {*} commonData - * @param {*} reportingService * @param {*} visFactoryService */ constructor( @@ -48,7 +47,6 @@ export class OverviewController { $rootScope, errorHandler, commonData, - reportingService, visFactoryService, $route, ) { @@ -59,7 +57,6 @@ export class OverviewController { this.errorHandler = errorHandler; this.tabVisualizations = new TabVisualizations(); this.commonData = commonData; - this.reportingService = reportingService; this.visFactoryService = visFactoryService; this.wazuhConfig = new WazuhConfig(); this.visFactoryService = VisFactoryHandler; @@ -327,13 +324,6 @@ export class OverviewController { this.$scope.$applyAsync(); } - /** - * Transform a visualization into an image - */ - startVis2Png() { - return this.reportingService.startVis2Png(this.tab); - } - /** * This fetch de agents summary */ diff --git a/plugins/main/public/services/index.js b/plugins/main/public/services/index.js index a82f4124c9..dea9069f72 100644 --- a/plugins/main/public/services/index.js +++ b/plugins/main/public/services/index.js @@ -14,7 +14,6 @@ import './theming'; import './routes'; import { CSVRequest } from './csv-request'; import { CommonData } from './common-data'; -import { ReportingService } from './reporting'; import { VisFactoryService } from './vis-factory-handler'; import './region-maps'; import './order-object-by'; @@ -28,7 +27,6 @@ app .service('errorHandler', ErrorHandler) .service('csvReq', CSVRequest) .service('commonData', CommonData) - .service('reportingService', ReportingService) .service('visFactoryService', VisFactoryService) .service('configHandler', ConfigHandler) .service('checkDaemonsStatus', CheckDaemonsStatus); diff --git a/plugins/main/public/services/reporting.js b/plugins/main/public/services/reporting.js deleted file mode 100644 index e44106bb66..0000000000 --- a/plugins/main/public/services/reporting.js +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Wazuh app - Reporting service - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import $ from 'jquery'; -import moment from 'moment'; -import { WazuhConfig } from '../react-services/wazuh-config'; -import { GenericRequest } from '../react-services/generic-request'; -import { ErrorHandler } from '../react-services/error-handler'; - -export class ReportingService { - constructor( - $rootScope, - vis2png, - rawVisualizations, - visHandlers, - errorHandler, - ) { - this.$rootScope = $rootScope; - this.vis2png = vis2png; - this.rawVisualizations = rawVisualizations; - this.visHandlers = visHandlers; - this.genericReq = GenericRequest; - this.errorHandler = errorHandler; - this.wazuhConfig = new WazuhConfig(); - } - removeTableVis(visList) { - const attributes = JSON.parse(visList.attributes.visState); - return attributes.type !== 'table'; - } - - removeAgentStatusVis(idArray) { - const monitoringEnabled = - this.wazuhConfig.getConfig()['wazuh.monitoring.enabled']; - if (!monitoringEnabled) { - const visArray = idArray.filter(vis => { - return vis !== 'Wazuh-App-Overview-General-Agents-status'; - }); - return visArray; - } - return idArray; - } - - async startVis2Png(tab, isAgents = false, syscollectorFilters = null) { - try { - if (this.vis2png.isWorking()) { - ErrorHandler.handle('Report in progress', 'Reporting', { - warning: true, - }); - return; - } - this.$rootScope.reportBusy = true; - this.$rootScope.reportStatus = 'Generating report...0%'; - this.$rootScope.$applyAsync(); - - this.vis2png.clear(); - - const rawVisualizations = this.rawVisualizations - .getList() - .filter(this.removeTableVis); - - let idArray = []; - if (tab === 'general') { - idArray = this.removeAgentStatusVis( - rawVisualizations.map(item => item.id), - ); - } else { - idArray = rawVisualizations.map(item => item.id); - } - - for (const item of idArray) { - const tmpHTMLElement = $(`#${item}`); - this.vis2png.assignHTMLItem(item, tmpHTMLElement); - } - - const appliedFilters = await this.visHandlers.getAppliedFilters( - syscollectorFilters, - ); - - const array = await this.vis2png.checkArray(idArray); - const name = `wazuh-${isAgents ? 'agents' : 'overview'}-${tab}-${ - (Date.now() / 1000) | 0 - }.pdf`; - - const browserTimezone = moment.tz.guess(true); - - const data = { - array, - name, - title: isAgents ? `Agents ${tab}` : `Overview ${tab}`, - filters: appliedFilters.filters, - time: appliedFilters.time, - searchBar: appliedFilters.searchBar, - tables: appliedFilters.tables, - tab, - section: isAgents ? 'agents' : 'overview', - isAgents, - browserTimezone, - }; - - await this.genericReq.request('POST', '/reports', data); - - this.$rootScope.reportBusy = false; - this.$rootScope.reportStatus = false; - this.$rootScope.$applyAsync(); - ErrorHandler.info( - 'Success. Go to Dashboard management > Reporting', - 'Reporting', - ); - - return; - } catch (error) { - this.$rootScope.reportBusy = false; - this.$rootScope.reportStatus = false; - throw error; - } - } - - async startConfigReport(obj, type, components) { - try { - this.$rootScope.reportBusy = true; - this.$rootScope.reportStatus = 'Generating PDF document...'; - this.$rootScope.$applyAsync(); - - const docType = - type === 'agentConfig' - ? `wazuh-agent-${obj.id}` - : `wazuh-group-${obj.name}`; - - const name = `${docType}-configuration-${(Date.now() / 1000) | 0}.pdf`; - const browserTimezone = moment.tz.guess(true); - - const data = { - array: [], - name, - filters: [ - type === 'agentConfig' ? { agent: obj.id } : { group: obj.name }, - ], - time: '', - searchBar: '', - tables: [], - tab: type, - browserTimezone, - components, - }; - - await this.genericReq.request('POST', '/reports', data); - - this.$rootScope.reportBusy = false; - this.$rootScope.reportStatus = false; - this.$rootScope.$applyAsync(); - ErrorHandler.info( - 'Success. Go to Dashboard management > Reporting', - 'Reporting', - ); - - return; - } catch (error) { - this.$rootScope.reportBusy = false; - this.$rootScope.reportStatus = false; - this.$rootScope.$applyAsync(); - throw error; - } - } -} From e39c12841997c4b1d05a6d24310d4d6f29dedb26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 11 Mar 2024 12:56:42 +0100 Subject: [PATCH 10/12] fix(reporting): fix agent inventory data report --- .../components/common/modules/main-agent.tsx | 25 +++++++++++++------ .../main/public/react-services/reporting.js | 7 +++--- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/plugins/main/public/components/common/modules/main-agent.tsx b/plugins/main/public/components/common/modules/main-agent.tsx index 6baa5d2ab9..70bc869e4a 100644 --- a/plugins/main/public/components/common/modules/main-agent.tsx +++ b/plugins/main/public/components/common/modules/main-agent.tsx @@ -25,7 +25,11 @@ import { AppState } from '../../../react-services/app-state'; import { ReportingService } from '../../../react-services/reporting'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; import { AgentInfo } from '../../common/welcome/agents-info'; -import { getAngularModule, getCore } from '../../../kibana-services'; +import { + getAngularModule, + getCore, + getDataPlugin, +} from '../../../kibana-services'; import { compose } from 'redux'; import { withGlobalBreadcrumb } from '../hocs'; import { endpointSummary } from '../../../utils/applications'; @@ -46,7 +50,7 @@ export class MainModuleAgent extends Component { constructor(props) { super(props); this.reportingService = new ReportingService(); - this.filterHandler = new FilterHandler(AppState.getCurrentPattern()); + this.filterHandler = new FilterHandler(AppState.getCurrentPattern()); // this.state = { selectView: false, loadingReport: false, @@ -70,14 +74,19 @@ export class MainModuleAgent extends Component { {} ).id || false; if (this.props.section === 'syscollector' && agent) { - syscollectorFilters.push(this.filterHandler.managerQuery(agent, true)); + syscollectorFilters.push(this.filterHandler.managerQuery(agent, true)); // This could be adding a filter with a wrong index name when the selected index has different title and id. Currently the index property is not used in the report of the Inventory data. syscollectorFilters.push(this.filterHandler.agentQuery(agent)); } - await this.reportingService.startVis2Png( - this.props.section, - agent, - syscollectorFilters.length ? syscollectorFilters : null, - ); + await this.reportingService.startVis2Png(this.props.section, agent, { + filters: syscollectorFilters, + time: { + from: 'now-1d/d', + to: 'now', + }, + indexPattern: await getDataPlugin().indexPatterns.get( + AppState.getCurrentPattern(), + ), + }); this.setState({ loadingReport: false }); } diff --git a/plugins/main/public/react-services/reporting.js b/plugins/main/public/react-services/reporting.js index 0db20a9d8e..a762e0a432 100644 --- a/plugins/main/public/react-services/reporting.js +++ b/plugins/main/public/react-services/reporting.js @@ -83,12 +83,13 @@ export class ReportingService { return store.getState().reportingReducers?.dataSourceSearchContext; } - async startVis2Png(tab, agents = false, syscollectorFilters = null) { + async startVis2Png(tab, agents = false, searchContext = null) { try { this.$rootScope.reportBusy = true; this.$rootScope.reportStatus = 'Generating report...0%'; this.$rootScope.$applyAsync(); - const dataSourceContext = await this.getDataSourceSearchContext(); + const dataSourceContext = + searchContext || (await this.getDataSourceSearchContext()); const visualizations = await this.getVisualizationsFromDOM(); const dataplugin = await getDataPlugin(); const serverSideQuery = dataplugin.query.getOpenSearchQuery(); @@ -99,7 +100,7 @@ export class ReportingService { serverSideQuery, // Used for applying the same filters on the server side requests filters: dataSourceContext.filters, time: dataSourceContext.time, - searchBar: dataSourceContext.query?.query || '', + searchBar: dataSourceContext?.query?.query || '', tables: [], // TODO: check is this is used tab, section: agents ? 'agents' : 'overview', From d0cc177881732489a9b2197fc3e2211023311816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 11 Mar 2024 12:57:32 +0100 Subject: [PATCH 11/12] fix(reporting): remove unused import --- plugins/main/public/react-services/reporting.js | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/main/public/react-services/reporting.js b/plugins/main/public/react-services/reporting.js index a762e0a432..4557a6aaa5 100644 --- a/plugins/main/public/react-services/reporting.js +++ b/plugins/main/public/react-services/reporting.js @@ -10,7 +10,6 @@ * Find more information about this on the LICENSE file. */ -import $ from 'jquery'; import moment from 'moment'; import { WazuhConfig } from '../react-services/wazuh-config'; import { AppState } from './app-state'; From 39716ba240d22cd912d78ae3b89f872bb488cd5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 12 Mar 2024 16:30:17 +0100 Subject: [PATCH 12/12] test(reporting): fix tests and remove unused services - Remove service to migrate the direcotry name of the PDF reports stored in the filesystem - Remove constants related to PDF report paths --- plugins/main/common/constants.ts | 10 - ...eporting-security-endpoint-handler.test.ts | 278 ----------------- ...ity-endpoint-parameters-validation.test.ts | 130 +------- plugins/main/server/plugin.ts | 12 - .../server/routes/wazuh-reporting.test.ts | 89 +----- .../server/start/migration-tasks/index.ts | 8 - .../reports_directory_name.test.ts | 284 ------------------ .../migration-tasks/reports_directory_name.ts | 82 ----- plugins/wazuh-core/common/constants.ts | 10 - 9 files changed, 5 insertions(+), 898 deletions(-) delete mode 100644 plugins/main/server/controllers/wazuh-reporting-security-endpoint-handler.test.ts delete mode 100644 plugins/main/server/start/migration-tasks/index.ts delete mode 100644 plugins/main/server/start/migration-tasks/reports_directory_name.test.ts delete mode 100644 plugins/main/server/start/migration-tasks/reports_directory_name.ts diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 8e0b1c9c93..5b641cbf4c 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -134,16 +134,6 @@ export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( 'wazuh-registry.json', ); -// Wazuh data path - downloads -export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join( - WAZUH_DATA_ABSOLUTE_PATH, - 'downloads', -); -export const WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH = path.join( - WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, - 'reports', -); - // Queue export const WAZUH_QUEUE_CRON_FREQ = '*/15 * * * * *'; // Every 15 seconds diff --git a/plugins/main/server/controllers/wazuh-reporting-security-endpoint-handler.test.ts b/plugins/main/server/controllers/wazuh-reporting-security-endpoint-handler.test.ts deleted file mode 100644 index 84a585d1ce..0000000000 --- a/plugins/main/server/controllers/wazuh-reporting-security-endpoint-handler.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import md5 from 'md5'; -import fs from 'fs'; -import { WazuhReportingCtrl } from './wazuh-reporting'; - -jest.mock('../lib/reporting/extended-information', () => ({ - extendedInformation: () => {}, - buildAgentsTable: () => {}, -})); - -jest.mock('../lib/reporting/printer', () => { - class ReportPrinterMock { - constructor() {} - addContent() {} - addConfigTables() {} - addTables() {} - addTimeRangeAndFilters() {} - addVisualizations() {} - formatDate() {} - checkTitle() {} - addSimpleTable() {} - addList() {} - addNewLine() {} - addContentWithNewLine() {} - addAgentsFilters() {} - print() {} - } - return { - ReportPrinter: ReportPrinterMock, - }; -}); - -const getMockerUserContext = (username: string) => ({ - username, - hashUsername: md5(username), -}); - -const mockContext = (username: string) => ({ - wazuh: { - security: { - getCurrentUser: () => getMockerUserContext(username), - }, - logger: { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - get: jest.fn(() => ({ - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - })), - }, - }, - wazuh_core: { - configuration: {}, - }, -}); - -const mockResponse = () => ({ - ok: body => body, - custom: body => body, - badRequest: body => body, -}); - -const endpointController = new WazuhReportingCtrl(); - -console.warn(` -Theses tests don't have in account the validation of endpoint parameters. -The endpoints related to an specific file. -This validation could prevent the endpoint handler is executed, so these tests -don't cover the reality. -`); - -describe('[security] Report endpoints guard related to a file. Parameter defines or builds the filename.', () => { - let routeHandler = null; - const routeHandlerResponse = 'Endpoint handler executed.'; - - beforeEach(() => { - routeHandler = jest.fn(() => routeHandlerResponse); - }); - - afterEach(() => { - routeHandler = null; - }); - - it.each` - testTitle | username | filename | endpointProtected - ${'Execute endpoint handler'} | ${'admin'} | ${'wazuh-module-overview-general-1234.pdf'} | ${false} - ${'Endpoint protected'} | ${'admin'} | ${'../wazuh-module-overview-general-1234.pdf'} | ${true} - ${'Endpoint protected'} | ${'admin'} | ${'wazuh-module-overview-../general-1234.pdf'} | ${true} - ${'Endpoint protected'} | ${'admin'} | ${'custom../wazuh-module-overview-general-1234.pdf'} | ${true} - ${'Execute endpoint handler'} | ${'../../etc'} | ${'wazuh-module-agents-001-general-1234.pdf'} | ${false} - ${'Endpoint protected'} | ${'../../etc'} | ${'../wazuh-module-agents-001-general-1234.pdf'} | ${true} - ${'Endpoint protected'} | ${'../../etc'} | ${'wazuh-module-overview-../general-1234.pdf'} | ${true} - ${'Endpoint protected'} | ${'../../etc'} | ${'custom../wazuh-module-overview-general-1234.pdf'} | ${true} - `( - `$testTitle - username: $username - filename: $filename - endpointProtected: $endpointProtected`, - async ({ username, filename, endpointProtected }) => { - const response = - await endpointController.checkReportsUserDirectoryIsValidRouteDecorator( - routeHandler, - function getFilename(request) { - return request.params.name; - }, - )( - mockContext(username), - { params: { name: filename } }, - mockResponse(), - ); - if (endpointProtected) { - expect(response.body.message).toBe('5040 - You shall not pass!'); - expect(routeHandler.mock.calls).toHaveLength(0); - } else { - expect(routeHandler.mock.calls).toHaveLength(1); - expect(response).toBe(routeHandlerResponse); - } - }, - ); -}); - -describe('[security] GET /reports', () => { - it.each` - username - ${'admin'} - ${'../../etc'} - `( - `Get user reports: GET /reports - username: $username`, - async ({ username }) => { - jest.spyOn(fs, 'readdirSync').mockImplementation(() => []); - - const response = await endpointController.getReports( - mockContext(username), - {}, - mockResponse(), - ); - expect(response.body.reports).toHaveLength(0); - }, - ); -}); - -describe('[security] GET /reports/{name}', () => { - it.each` - titleTest | username | filename | valid - ${'Get report'} | ${'admin'} | ${'wazuh-module-overview-1234.pdf'} | ${true} - ${'Endpoint protected'} | ${'admin'} | ${'../wazuh-module-overview-1234.pdf'} | ${false} - ${'Get report'} | ${'../../etc'} | ${'wazuh-module-overview-1234.pdf'} | ${true} - ${'Endpoint protected'} | ${'../../etc'} | ${'../wazuh-module-overview-1234.pdf'} | ${false} - `( - `$titleTest: GET /reports/$filename - username: $username - valid: $valid`, - async ({ username, filename, valid }) => { - const fileContent = 'content file'; - jest.spyOn(fs, 'readFileSync').mockImplementation(() => fileContent); - - const response = await endpointController.getReportByName( - mockContext(username), - { params: { name: filename } }, - mockResponse(), - ); - if (valid) { - expect(response.headers['Content-Type']).toBe('application/pdf'); - expect(response.body).toBe('content file'); - } else { - expect(response.body.message).toBe('5040 - You shall not pass!'); - } - }, - ); -}); - -describe('[security] POST /reports', () => { - jest.mock('../lib/filesystem', () => ({ - createDataDirectoryIfNotExists: jest.fn(), - })); - - it.each` - titleTest | username | moduleID | valid - ${'Create report'} | ${'admin'} | ${'general'} | ${true} - ${'Endpoint protected'} | ${'admin'} | ${'../general'} | ${false} - ${'Create report'} | ${'../../etc'} | ${'general'} | ${true} - ${'Endpoint protected'} | ${'../../etc'} | ${'../general'} | ${false} - `( - `$titleTest: POST /reports/modules/$moduleID - username: $username - valid: $valid`, - async ({ username, moduleID, valid }) => { - jest - .spyOn(endpointController, 'renderHeader') - .mockImplementation(() => true); - jest - .spyOn(endpointController, 'sanitizeKibanaFilters') - .mockImplementation(() => [false, false]); - - const mockRequest = { - body: { - array: [], - agents: false, - browserTimezone: '', - searchBar: '', - filters: [], - time: { - from: '', - to: '', - }, - tables: [], - section: 'overview', - indexPatternTitle: 'wazuh-alerts-*', - apiId: 'default', - tab: moduleID, - }, - params: { - moduleID: moduleID, - }, - }; - - const response = await endpointController.createReportsModules( - mockContext(username), - mockRequest, - mockResponse(), - ); - - if (valid) { - expect(response.body.success).toBe(true); - expect(response.body.message).toMatch( - new RegExp(`Report wazuh-module-overview-${moduleID}`), - ); - } else { - expect(response.body.message).toBe('5040 - You shall not pass!'); - } - }, - ); -}); - -describe('[security] DELETE /reports/', () => { - let mockFsUnlinkSync; - - afterEach(() => { - mockFsUnlinkSync.mockClear(); - }); - - it.each` - titleTest | username | filename | valid - ${'Delete report'} | ${'admin'} | ${'wazuh-module-overview-1234.pdf'} | ${true} - ${'Endpoint protected'} | ${'admin'} | ${'../wazuh-module-overview-1234.pdf'} | ${false} - ${'Endpoint protected'} | ${'admin'} | ${'custom../wazuh-module-overview-1234.pdf'} | ${false} - ${'Delete report'} | ${'../../etc'} | ${'wazuh-module-overview-1234.pdf'} | ${true} - ${'Endpoint protected'} | ${'../../etc'} | ${'../wazuh-module-overview-1234.pdf'} | ${false} - ${'Endpoint protected'} | ${'../../etc'} | ${'custom../wazuh-module-overview-1234.pdf'} | ${false} - `( - `[security] DELETE /reports/$filename - username: $username - valid: $valid`, - async ({ filename, username, valid }) => { - mockFsUnlinkSync = jest - .spyOn(fs, 'unlinkSync') - .mockImplementation(() => {}); - - const response = await endpointController.deleteReportByName( - mockContext(username), - { params: { name: filename } }, - mockResponse(), - ); - - if (valid) { - expect(response.body.error).toBe(0); - expect(mockFsUnlinkSync.mock.calls).toHaveLength(1); - } else { - expect(response.body.message).toBe('5040 - You shall not pass!'); - expect(mockFsUnlinkSync.mock.calls).toHaveLength(0); - } - }, - ); -}); diff --git a/plugins/main/server/controllers/wazuh-reporting-security-endpoint-parameters-validation.test.ts b/plugins/main/server/controllers/wazuh-reporting-security-endpoint-parameters-validation.test.ts index 4aa002af30..61c3c8d2f0 100644 --- a/plugins/main/server/controllers/wazuh-reporting-security-endpoint-parameters-validation.test.ts +++ b/plugins/main/server/controllers/wazuh-reporting-security-endpoint-parameters-validation.test.ts @@ -5,18 +5,6 @@ import { ByteSizeValue } from '@osd/config-schema'; import supertest from 'supertest'; import { WazuhReportingRoutes } from '../routes/wazuh-reporting'; import md5 from 'md5'; -import { - createDataDirectoryIfNotExists, - createDirectoryIfNotExists, -} from '../lib/filesystem'; -import { - WAZUH_DATA_ABSOLUTE_PATH, - WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, -} from '../../common/constants'; -import { execSync } from 'child_process'; -import path from 'path'; -import fs from 'fs'; const loggingService = loggingSystemMock.create(); const logger = loggingService.get(); @@ -45,40 +33,6 @@ const enhanceWithContext = (fn: (...args: any[]) => any) => let server, innerServer; beforeAll(async () => { - // Create /data/wazuh directory. - createDataDirectoryIfNotExists(); - // Create /data/wazuh/downloads directory. - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH); - // Create /data/wazuh/downloads/reports directory. - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH); - // Create report files - [ - { name: md5('admin'), files: ['wazuh-module-overview-general-1234.pdf'] }, - { - name: md5('../../etc'), - files: ['wazuh-module-overview-general-1234.pdf'], - }, - ].forEach(({ name, files }) => { - createDirectoryIfNotExists( - path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, name), - ); - - if (files) { - files.forEach(filename => - fs.closeSync( - fs.openSync( - path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - name, - filename, - ), - 'w', - ), - ), - ); - } - }); - // Create server const config = { name: 'plugin_platform', @@ -112,63 +66,10 @@ beforeAll(async () => { }); afterAll(async () => { - // Remove /data/wazuh directory. - execSync(`rm -rf ${WAZUH_DATA_ABSOLUTE_PATH}`); - // Stop server await server.stop(); }); -describe('[endpoint] GET /reports', () => { - it.each` - username - ${'admin'} - ${'../../etc'} - `( - `Get reports of user GET /reports - 200 - username: $username`, - async ({ username }) => { - const response = await supertest(innerServer.listener) - .get('/reports') - .set('x-test-username', username) - .expect(200); - - expect(response.body.reports).toBeDefined(); - }, - ); -}); - -describe('[endpoint][security] GET /reports/{name} - Parameters validation', () => { - it.each` - testTitle | username | filename | responseStatusCode | responseBodyMessage - ${'Get report by filename'} | ${'admin'} | ${'wazuh-module-overview-general-1234.pdf'} | ${200} | ${null} - ${'Invalid parameters'} | ${'admin'} | ${'..%2fwazuh-module-overview-general-1234.pdf'} | ${400} | ${'[request params.name]: must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.'} - ${'Invalid parameters'} | ${'admin'} | ${'custom..%2fwazuh-module-overview-general-1234.pdf'} | ${400} | ${'[request params.name]: must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.'} - ${'Route not found'} | ${'admin'} | ${'../custom..%2fwazuh-module-overview-general-1234.pdf'} | ${404} | ${/Not Found/} - ${'Get report by filename'} | ${'../../etc'} | ${'wazuh-module-overview-general-1234.pdf'} | ${200} | ${null} - ${'Invalid parameters'} | ${'../../etc'} | ${'..%2fwazuh-module-overview-general-1234.pdf'} | ${400} | ${'[request params.name]: must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.'} - ${'Invalid parameters'} | ${'../../etc'} | ${'custom..%2fwazuh-module-overview-general-1234.pdf'} | ${400} | ${'[request params.name]: must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.'} - ${'Route not found'} | ${'../../etc'} | ${'../custom..%2fwazuh-module-overview-general-1234.pdf'} | ${404} | ${/Not Found/} - `( - `$testTitle: GET /reports/$filename - responseStatusCode: $responseStatusCode - username: $username - responseBodyMessage: $responseBodyMessage`, - async ({ username, filename, responseStatusCode, responseBodyMessage }) => { - const response = await supertest(innerServer.listener) - .get(`/reports/${filename}`) - .set('x-test-username', username) - .expect(responseStatusCode); - if (responseStatusCode === 200) { - expect(response.header['content-type']).toMatch(/application\/pdf/); - expect(response.body instanceof Buffer).toBe(true); - } - if (responseBodyMessage) { - expect(response.body.message).toMatch(responseBodyMessage); - } - }, - ); -}); - describe('[endpoint][security] POST /reports/modules/{moduleID} - Parameters validation', () => { it.each` testTitle | username | moduleID | agents | responseStatusCode | responseBodyMessage @@ -225,7 +126,7 @@ describe('[endpoint][security] POST /reports/groups/{groupID} - Parameters valid ${'Invalid parameters'} | ${'../../etc'} | ${'..%2fdefault'} | ${400} | ${'[request params.groupID]: must be A-z, 0-9, _, . are allowed. It must not be ., .. or all.'} ${'Route not found'} | ${'../../etc'} | ${'../default'} | ${404} | ${/Not Found/} `( - `$testTitle: GET /reports/groups/$groupID - $responseStatusCode + `$testTitle: POST /reports/groups/$groupID - $responseStatusCode username: $username responseBodyMessage: $responseBodyMessage`, async ({ username, groupID, responseStatusCode, responseBodyMessage }) => { @@ -258,7 +159,7 @@ describe('[endpoint][security] POST /reports/agents/{agentID} - Parameters valid ${'Invalid parameters'} | ${'../../etc'} | ${'..%2f001'} | ${400} | ${/\[request params.agentID\]: must be 0-9 are allowed/} ${'Invalid parameters'} | ${'../../etc'} | ${'1'} | ${400} | ${/\[request params.agentID\]: value has length \[1\] but it must have a minimum length of \[3\]./} `( - `$testTitle: GET /reports/agents/$agentID - $responseStatusCode + `$testTitle: POST /reports/agents/$agentID - $responseStatusCode username: $username responseBodyMessage: $responseBodyMessage`, async ({ username, agentID, responseStatusCode, responseBodyMessage }) => { @@ -320,30 +221,3 @@ describe('[endpoint][security] POST /reports/agents/{agentID}/inventory - Parame }, ); }); - -describe('[endpoint][security] DELETE /reports/{name} - Parameters validation', () => { - it.each` - testTitle | username | filename | responseStatusCode | responseBodyMessage - ${'Delete report file'} | ${'admin'} | ${'wazuh-module-overview-general-1234.pdf'} | ${200} | ${null} - ${'Invalid parameters'} | ${'admin'} | ${'..%2fwazuh-module-overview-general-1234.pdf'} | ${400} | ${'[request params.name]: must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.'} - ${'Invalid parameters'} | ${'admin'} | ${'custom..%2fwazuh-module-overview-general-1234.pdf'} | ${400} | ${'[request params.name]: must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.'} - ${'Route not found'} | ${'admin'} | ${'../wazuh-module-overview-general-1234.pdf'} | ${404} | ${/Not Found/} - ${'Delete report file'} | ${'../../etc'} | ${'wazuh-module-overview-general-1234.pdf'} | ${200} | ${null} - ${'Invalid parameters'} | ${'../../etc'} | ${'..%2fwazuh-module-overview-general-1234.pdf'} | ${400} | ${'[request params.name]: must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.'} - ${'Invalid parameters'} | ${'../../etc'} | ${'custom..%2fwazuh-module-overview-general-1234.pdf'} | ${400} | ${'[request params.name]: must be A-z, 0-9, _, ., and - are allowed. It must end with .pdf.'} - ${'Route not found'} | ${'../../etc'} | ${'../wazuh-module-overview-general-1234.pdf'} | ${404} | ${/Not Found/} - `( - `$testTitle: DELETE /reports/$filename - $responseStatusCode - username: $username - responseBodyMessage: $responseBodyMessage`, - async ({ username, filename, responseStatusCode, responseBodyMessage }) => { - const response = await supertest(innerServer.listener) - .delete(`/reports/${filename}`) - .set('x-test-username', username) - .expect(responseStatusCode); - if (responseBodyMessage) { - expect(response.body.message).toMatch(responseBodyMessage); - } - }, - ); -}); diff --git a/plugins/main/server/plugin.ts b/plugins/main/server/plugin.ts index 3a0b4ad4f5..b0f26b9309 100644 --- a/plugins/main/server/plugin.ts +++ b/plugins/main/server/plugin.ts @@ -33,7 +33,6 @@ import { jobMonitoringRun, jobSchedulerRun, jobQueueRun, - jobMigrationTasksRun, } from './start'; import { first } from 'rxjs/operators'; @@ -132,17 +131,6 @@ export class WazuhPlugin implements Plugin { server: contextServer, }); - // Migration tasks - jobMigrationTasksRun({ - core, - wazuh: { - logger: this.logger.get('migration-task'), - api: plugins.wazuhCore.api, - }, - wazuh_core: plugins.wazuhCore, - server: contextServer, - }); - // Monitoring jobMonitoringRun({ core, diff --git a/plugins/main/server/routes/wazuh-reporting.test.ts b/plugins/main/server/routes/wazuh-reporting.test.ts index c4dc4f4765..4689b6af46 100644 --- a/plugins/main/server/routes/wazuh-reporting.test.ts +++ b/plugins/main/server/routes/wazuh-reporting.test.ts @@ -7,21 +7,7 @@ import { ByteSizeValue } from '@osd/config-schema'; import supertest from 'supertest'; import { WazuhUtilsRoutes } from './wazuh-utils'; import { WazuhReportingRoutes } from './wazuh-reporting'; -import { WazuhUtilsCtrl } from '../controllers/wazuh-utils/wazuh-utils'; import md5 from 'md5'; -import path from 'path'; -import { - createDataDirectoryIfNotExists, - createDirectoryIfNotExists, -} from '../lib/filesystem'; -import { - WAZUH_DATA_CONFIG_DIRECTORY_PATH, - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - WAZUH_DATA_ABSOLUTE_PATH, - WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, -} from '../../common/constants'; -import { execSync } from 'child_process'; -import fs from 'fs'; jest.mock('../lib/reporting/extended-information', () => ({ extendedInformation: jest.fn(), @@ -81,12 +67,6 @@ let server, innerServer; // BEFORE ALL beforeAll(async () => { - // Create /data/wazuh directory. - createDataDirectoryIfNotExists(); - - // Create /data/wazuh/config directory. - createDirectoryIfNotExists(WAZUH_DATA_CONFIG_DIRECTORY_PATH); - // Create server const config = { name: 'plugin_platform', @@ -134,64 +114,6 @@ afterAll(async () => { // Clear all mocks jest.clearAllMocks(); - - // Remove /data/wazuh directory. - execSync(`rm -rf ${WAZUH_DATA_ABSOLUTE_PATH}`); -}); - -describe('[endpoint] GET /reports', () => { - const directories = [ - { username: 'admin', files: 0 }, - { username: '../../etc', files: 1 }, - ]; - beforeAll(() => { - // Create /data/wazuh directory. - createDataDirectoryIfNotExists(); - - // Create /data/wazuh/downloads directory. - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH); - - // Create /data/wazuh/downloads/reports directory. - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH); - - // Create directories and file/s within directory. - directories.forEach(({ username, files }) => { - const hashUsername = md5(username); - createDirectoryIfNotExists( - path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, hashUsername), - ); - if (files) { - Array.from(Array(files).keys()).forEach(indexFile => { - fs.closeSync( - fs.openSync( - path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - hashUsername, - `report_${indexFile}.pdf`, - ), - 'w', - ), - ); - }); - } - }); - }); - - afterAll(async () => { - // Remove /data/wazuh/downloads directory. - execSync(`rm -rf ${WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH}`); - }); - - it.each(directories)( - 'get reports of $username. status response: $responseStatus', - async ({ username, files }) => { - const response = await supertest(innerServer.listener) - .get(`/reports`) - .set('x-test-username', username) - .expect(200); - expect(response.body.reports).toHaveLength(files); - }, - ); }); describe('[endpoint] PUT /utils/configuration', () => { @@ -322,15 +244,10 @@ describe('[endpoint] PUT /utils/configuration', () => { const responseReport = await supertest(innerServer.listener) .post(`/reports/modules/${tab}`) .set('x-test-username', USER_NAME) - .send(reportBody); - // .expect(200); + .send(reportBody) + .expect(200); - const fileName = - responseReport.body?.message.match(/([A-Z-0-9]*\.pdf)/gi)[0]; - const userPath = md5(USER_NAME); - const reportPath = `${WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH}/${userPath}/${fileName}`; - const PDFbuffer = fs.readFileSync(reportPath); - const PDFcontent = PDFbuffer.toString('utf8'); + const PDFcontent = responseReport.body.toString('utf8'); const content = PDFcontent.replace( /\[<[a-z0-9].+> <[a-z0-9].+>\]/gi, '', diff --git a/plugins/main/server/start/migration-tasks/index.ts b/plugins/main/server/start/migration-tasks/index.ts deleted file mode 100644 index 82ecf2f19c..0000000000 --- a/plugins/main/server/start/migration-tasks/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import migrateReportsDirectoryName from './reports_directory_name'; - -export function jobMigrationTasksRun(context) { - context.wazuh.logger.debug('Migration tasks started'); - const migrationTasks = [migrateReportsDirectoryName]; - - migrationTasks.forEach(task => task(context)); -} diff --git a/plugins/main/server/start/migration-tasks/reports_directory_name.test.ts b/plugins/main/server/start/migration-tasks/reports_directory_name.test.ts deleted file mode 100644 index 226fe4123e..0000000000 --- a/plugins/main/server/start/migration-tasks/reports_directory_name.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import fs from 'fs'; -import md5 from 'md5'; -import { execSync } from 'child_process'; -import path from 'path'; -import { - WAZUH_DATA_ABSOLUTE_PATH, - WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, -} from '../../../common/constants'; -import { - createDataDirectoryIfNotExists, - createDirectoryIfNotExists, -} from '../../lib/filesystem'; -import migrateReportsDirectoryName, { isMD5 } from './reports_directory_name'; - -function mockContextCreator(loggerLevel: string) { - const logs = []; - const levels = ['debug', 'info', 'warn', 'error']; - - function createLogger(level: string) { - return jest.fn(function (message: string) { - const levelLogIncluded: number = levels.findIndex( - level => level === loggerLevel, - ); - levelLogIncluded > -1 && - levels.slice(levelLogIncluded).includes(level) && - logs.push({ level, message }); - }); - } - - const ctx = { - wazuh: { - logger: { - info: createLogger('info'), - warn: createLogger('warn'), - error: createLogger('error'), - debug: createLogger('debug'), - }, - }, - /* Mocked logs getter. It is only for testing purpose.*/ - _getLogs(logLevel: string) { - return logLevel ? logs.filter(({ level }) => level === logLevel) : logs; - }, - }; - return ctx; -} - -beforeAll(() => { - // Create /data/wazuh directory. - createDataDirectoryIfNotExists(); - // Create /data/wazuh/downloads directory. - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH); -}); - -afterAll(() => { - // Remove /data/wazuh directory. - execSync(`rm -rf ${WAZUH_DATA_ABSOLUTE_PATH}`); -}); - -describe("[migration] `reports` directory doesn't exist", () => { - let mockContext = mockContextCreator('debug'); - - it("Debug mode - Task started and skipped because of the `reports` directory doesn't exit", () => { - // Migrate the directories - migrateReportsDirectoryName(mockContext); - // Logs that the task started and skipped. - expect( - mockContext - ._getLogs('debug') - .filter(({ message }) => message.includes('Task started')), - ).toHaveLength(1); - expect( - mockContext - ._getLogs('debug') - .filter(({ message }) => - message.includes( - "Reports directory doesn't exist. The task is not required. Skip.", - ), - ), - ).toHaveLength(1); - }); -}); - -describe('[migration] Rename the subdirectories of `reports` directory', () => { - let mockContext = null; - - beforeEach(() => { - mockContext = mockContextCreator('info'); - // Create /data/wazuh/downloads/reports directory. - createDirectoryIfNotExists(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH); - }); - - afterEach(() => { - mockContext = null; - execSync(`rm -rf ${WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH}`); - }); - - const userNameDirectory1 = { name: 'user1', files: 0 }; - const userNameDirectory2 = { name: 'user2', files: 0 }; - const userNameDirectory3 = { name: 'user3', files: 0 }; - const userNameDirectory4 = { name: 'user4', files: 0 }; - const userNameDirectory1MD5 = { name: md5('user1'), files: 0 }; - const userNameDirectory1MD5WithFiles = { name: md5('user1'), files: 1 }; - const userNameDirectory2MD5WithFiles = { name: md5('user2'), files: 1 }; - const userNameDirectory1WithFiles = { name: 'user1', files: 1 }; - const userNameDirectory2WithFiles = { name: 'user2', files: 0 }; - - const userDirectoriesTest1 = []; - const userDirectoriesTest2 = [userNameDirectory1, userNameDirectory2]; - const userDirectoriesTest3 = [ - userNameDirectory1, - userNameDirectory2, - userNameDirectory3, - userNameDirectory4, - ]; - const userDirectoriesTest4 = [userNameDirectory1, userNameDirectory1MD5]; - const userDirectoriesTest5 = [ - { ...userNameDirectory1, errorRenaming: true }, - userNameDirectory1MD5WithFiles, - userNameDirectory2, - ]; - const userDirectoriesTest6 = [ - { ...userNameDirectory1, errorRenaming: true }, - userNameDirectory1MD5WithFiles, - { ...userNameDirectory2, errorRenaming: true }, - userNameDirectory2MD5WithFiles, - ]; - const userDirectoriesTest7 = [ - userNameDirectory1WithFiles, - userNameDirectory2WithFiles, - ]; - const userDirectoriesTest8 = [ - userNameDirectory1MD5WithFiles, - userNameDirectory2MD5WithFiles, - ]; - - function formatUserDirectoriesTest(inputs: any) { - return inputs.length - ? inputs - .map( - input => - `[${input.name}:${input.files}${ - input.errorRenaming ? ' (Error: renaming)' : '' - }]`, - ) - .join(', ') - : 'None'; - } - - it.each` - directories | foundRequireRenamingDirectories | renamedDirectories | title - ${userDirectoriesTest1} | ${0} | ${0} | ${formatUserDirectoriesTest(userDirectoriesTest1)} - ${userDirectoriesTest2} | ${2} | ${2} | ${formatUserDirectoriesTest(userDirectoriesTest2)} - ${userDirectoriesTest3} | ${4} | ${4} | ${formatUserDirectoriesTest(userDirectoriesTest3)} - ${userDirectoriesTest4} | ${1} | ${1} | ${formatUserDirectoriesTest(userDirectoriesTest4)} - ${userDirectoriesTest5} | ${2} | ${1} | ${formatUserDirectoriesTest(userDirectoriesTest5)} - ${userDirectoriesTest6} | ${2} | ${0} | ${formatUserDirectoriesTest(userDirectoriesTest6)} - ${userDirectoriesTest7} | ${2} | ${2} | ${formatUserDirectoriesTest(userDirectoriesTest7)} - ${userDirectoriesTest8} | ${0} | ${0} | ${formatUserDirectoriesTest(userDirectoriesTest8)} - `( - 'Migrate Directories: $title - FoundRequireRenamingDirectories: $foundRequireRenamingDirectories - renamedDirectories: $renamedDirectories.', - ({ directories, foundRequireRenamingDirectories, renamedDirectories }) => { - const errorRenamingDirectoryMessages = - foundRequireRenamingDirectories - renamedDirectories; - // Create directories and file/s within directory. - directories.forEach(({ name, files }) => { - createDirectoryIfNotExists( - path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, name), - ); - if (files) { - Array.from(Array(files).keys()).forEach(indexFile => { - fs.closeSync( - fs.openSync( - path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - name, - `report_${indexFile}.pdf`, - ), - 'w', - ), - ); - }); - } - }); - - // Migrate the directories. - migrateReportsDirectoryName(mockContext); - - // Check the quantity of directories were found for renaming renaming. - expect( - mockContext - ._getLogs() - .filter(({ message }) => - message.includes('Found reports directory to migrate'), - ), - ).toHaveLength(foundRequireRenamingDirectories); - // Check the quantity of directories were renamed. - expect( - mockContext - ._getLogs() - .filter(({ message }) => message.includes('Renamed directory [')), - ).toHaveLength(renamedDirectories); - expect( - mockContext - ._getLogs('error') - .filter(({ message }) => - message.includes(`Error renaming directory [`), - ), - ).toHaveLength(errorRenamingDirectoryMessages); - - directories.forEach(({ name, ...rest }) => { - if (!rest.errorRenaming) { - if (isMD5(name)) { - // If directory name is a valid MD5, the directory should exist. - expect( - fs.existsSync( - path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, name), - ), - ).toBe(true); - } else { - // If directory name is not a valid MD5, the directory should be renamed. New directory exists and old directory doesn't exist. - expect( - mockContext - ._getLogs() - .filter(({ message }) => - message.includes(`Renamed directory [${name}`), - ), - ).toHaveLength(1); - expect( - mockContext - ._getLogs() - .filter(({ message }) => - message.includes( - `Found reports directory to migrate: [${name}`, - ), - ), - ).toHaveLength(1); - expect( - fs.existsSync( - path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - md5(name), - ), - ), - ).toBe(true); - expect( - !fs.existsSync( - path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, name), - ), - ).toBe(true); - } - } else { - // Check there was an error renaming the directory because of the directory exist and contains files. - expect( - mockContext - ._getLogs() - .filter(({ message }) => - message.includes( - `Found reports directory to migrate: [${name}`, - ), - ), - ).toHaveLength(1); - expect( - mockContext - ._getLogs('error') - .some(({ message }) => - message.includes(`Error renaming directory [${name}`), - ), - ).toBe(true); - expect( - fs.existsSync( - path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, name), - ), - ).toBe(true); - expect( - fs.existsSync( - path.join(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, md5(name)), - ), - ).toBe(true); - } - }); - }, - ); -}); diff --git a/plugins/main/server/start/migration-tasks/reports_directory_name.ts b/plugins/main/server/start/migration-tasks/reports_directory_name.ts deleted file mode 100644 index 288f8524f2..0000000000 --- a/plugins/main/server/start/migration-tasks/reports_directory_name.ts +++ /dev/null @@ -1,82 +0,0 @@ -import fs from 'fs'; -import md5 from 'md5'; -import path from 'path'; -import { WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH } from '../../../common/constants'; - -/** - * This task renames the report user folder from username to hashed username. - * @param context - * @returns - */ -export default function migrateReportsDirectoryName(context) { - // Create a wrapper function that logs to plugin files and platform logging system - const createLog = (level: string) => message => { - context.wazuh.logger[level](`migration:reportsDirectoryName: ${message}`); - }; - - // Create the logger - const logger = { - info: createLog('info'), - warn: createLog('warn'), - error: createLog('error'), - debug: createLog('debug'), - }; - - try { - logger.debug('Task started: Migrate reports directory name'); - - // Skip the task if the directory that stores the reports files doesn't exist in the file system - if (!fs.existsSync(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH)) { - logger.debug( - "Reports directory doesn't exist. The task is not required. Skip.", - ); - return; - } - - // Read the directories/files in the reports path - logger.debug( - `Reading reports directory: ${WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH}`, - ); - fs.readdirSync(WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, { - withFileTypes: true, - }).forEach(fileDirent => { - // If it is a directory and has not a valid MD5 hash, continue the task. - if (fileDirent.isDirectory() && !isMD5(fileDirent.name)) { - // Generate the origin and target path and hash the name - const originDirectoryPath = path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - fileDirent.name, - ); - const targetDirectoryName = md5(fileDirent.name); - const targetDirectoryPath = path.join( - WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, - targetDirectoryName, - ); - try { - logger.info( - `Found reports directory to migrate: [${fileDirent.name}]`, - ); - // Rename the directory from origin to target path - fs.renameSync(originDirectoryPath, targetDirectoryPath); - logger.info( - `Renamed directory [${fileDirent.name} (${originDirectoryPath})] to [${targetDirectoryName} (${targetDirectoryPath})]`, - ); - } catch (error) { - logger.error( - `Error renaming directory [${fileDirent.name} (${originDirectoryPath})] to [${targetDirectoryName} (${targetDirectoryPath})]: ${error.message}`, - ); - } - } - }); - logger.debug('Task finished: Migrate reports directory name'); - } catch (error) { - logger.error(`Error: ${error.message}`); - } -} - -// Check that the text is a valid MD5 hash -// https://melvingeorge.me/blog/check-if-string-is-valid-md5-hash-javascript -export function isMD5(text: string) { - const regexMD5 = /^[a-f0-9]{32}$/gi; - return regexMD5.test(text); -} diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 10e3e128ab..314dae4e55 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -129,16 +129,6 @@ export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( 'wazuh-registry.json', ); -// Wazuh data path - downloads -export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join( - WAZUH_DATA_ABSOLUTE_PATH, - 'downloads', -); -export const WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH = path.join( - WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, - 'reports', -); - // Queue export const WAZUH_QUEUE_CRON_FREQ = '*/15 * * * * *'; // Every 15 seconds