diff --git a/package.json b/package.json index 36398fae84..007d64bed1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "ansicolors": "^0.3.2", "install": "^0.10.1", "js-yaml": "3.10.0", + "json2csv": "^4.1.2", "lodash": "3.10.1", "needle": "^2.0.1", "node-cron": "^1.1.2", diff --git a/public/app.js b/public/app.js index 7bf02e61dd..0ee8c3855e 100644 --- a/public/app.js +++ b/public/app.js @@ -56,6 +56,7 @@ import 'plugins/wazuh/services/data-handler.js'; import 'plugins/wazuh/services/app-state.js'; import 'plugins/wazuh/services/api-tester.js'; import 'plugins/wazuh/services/pattern-handler.js'; +import 'plugins/wazuh/services/csv-request.js'; // Set up routes and views import 'plugins/wazuh/services/routes.js'; diff --git a/public/controllers/agents-preview.js b/public/controllers/agents-preview.js index f7f39c28bd..e4c1d36cf6 100644 --- a/public/controllers/agents-preview.js +++ b/public/controllers/agents-preview.js @@ -10,10 +10,11 @@ * Find more information about this on the LICENSE file. */ import * as modules from 'ui/modules' +import CsvGenerator from './csv-generator' const app = modules.get('app/wazuh', []); -app.controller('agentsPreviewController', function ($scope, $rootScope, $routeParams, genericReq, apiReq, appState, Agents, $location, errorHandler) { +app.controller('agentsPreviewController', function ($scope, $rootScope, $routeParams, genericReq, apiReq, appState, Agents, $location, errorHandler, csvReq) { $scope.loading = true; $scope.agents = Agents; $scope.status = 'all'; @@ -80,6 +81,18 @@ app.controller('agentsPreviewController', function ($scope, $rootScope, $routePa } } + $scope.downloadCsv = async () => { + try { + const currentApi = JSON.parse(appState.getCurrentAPI()).id; + const output = await csvReq.fetch('/agents', currentApi, $scope.agents ? $scope.agents.filters : null); + const csvGenerator = new CsvGenerator(output.csv, 'agents.csv'); + csvGenerator.download(true); + } catch (error) { + errorHandler.handle(error,'Download CSV'); + if(!$rootScope.$$phase) $rootScope.$digest(); + } + } + const load = async () => { try{ const data = await Promise.all([ diff --git a/public/controllers/csv-generator.js b/public/controllers/csv-generator.js new file mode 100644 index 0000000000..681ccb7ff3 --- /dev/null +++ b/public/controllers/csv-generator.js @@ -0,0 +1,34 @@ +/* + * Wazuh app - CSV file generator + * Copyright (C) 2018 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. + */ +export default class CsvGenerator { + + constructor (dataArray, fileName) { + this.dataArray = dataArray; + this.fileName = fileName; + } + + getLinkElement (linkText) { + return this.linkElement = this.linkElement || $('' + (linkText || '') + '', { + href: 'data:attachment/csv;base64,' + encodeURI(btoa(this.dataArray)), + target: '_blank', + download: this.fileName + }); + } + + download (removeAfterDownload) { + this.getLinkElement().css('display', 'none').appendTo('body'); + this.getLinkElement()[0].click(); + if (removeAfterDownload) { + this.getLinkElement().remove(); + } + } +} \ No newline at end of file diff --git a/public/controllers/groups.js b/public/controllers/groups.js index bb6545f9e4..1c34641d9f 100644 --- a/public/controllers/groups.js +++ b/public/controllers/groups.js @@ -11,12 +11,13 @@ */ import beautifier from 'plugins/wazuh/utils/json-beautifier'; import * as modules from 'ui/modules' +import CsvGenerator from './csv-generator' const app = modules.get('app/wazuh', []); // Groups preview controller app.controller('groupsPreviewController', -function ($scope, $rootScope, $location, apiReq, Groups, GroupFiles, GroupAgents, errorHandler) { +function ($scope, $rootScope, $location, apiReq, Groups, GroupFiles, GroupAgents, errorHandler, csvReq, appState) { const reloadWatcher = $rootScope.$watch('groupsIsReloaded',() => { delete $rootScope.groupsIsReloaded; $scope.lookingGroup = false; @@ -31,6 +32,19 @@ function ($scope, $rootScope, $location, apiReq, Groups, GroupFiles, GroupAgents $scope.groupAgents = GroupAgents; $scope.groupFiles = GroupFiles; + $scope.downloadCsv = async dataProvider => { + try { + const path = $scope[dataProvider] ? $scope[dataProvider].path : null; + const currentApi = JSON.parse(appState.getCurrentAPI()).id; + const output = await csvReq.fetch(path, currentApi, $scope[dataProvider] ? $scope[dataProvider].filters : null); + const csvGenerator = new CsvGenerator(output.csv, 'groups.csv'); + csvGenerator.download(true); + } catch (error) { + errorHandler.handle(error,'Download CSV'); + if(!$rootScope.$$phase) $rootScope.$digest(); + } + } + // Store a boolean variable to check if come from agents const fromAgents = ('comeFrom' in $rootScope) && ('globalAgent' in $rootScope) && $rootScope.comeFrom === 'agents'; diff --git a/public/controllers/osseclog.js b/public/controllers/osseclog.js index df37436aaf..c83f83f854 100644 --- a/public/controllers/osseclog.js +++ b/public/controllers/osseclog.js @@ -10,11 +10,12 @@ * Find more information about this on the LICENSE file. */ import * as modules from 'ui/modules' +import CsvGenerator from './csv-generator' const app = modules.get('app/wazuh', []); // Logs controller -app.controller('managerLogController', function ($scope, $rootScope, Logs, apiReq, errorHandler) { +app.controller('managerLogController', function ($scope, $rootScope, Logs, apiReq, errorHandler, csvReq, appState) { $scope.searchTerm = ''; $scope.loading = true; $scope.logs = Logs; @@ -48,8 +49,20 @@ app.controller('managerLogController', function ($scope, $rootScope, Logs, apiRe if(!$scope.$$phase) $scope.$digest(); } + $scope.downloadCsv = async () => { + try { + const currentApi = JSON.parse(appState.getCurrentAPI()).id; + const output = await csvReq.fetch('/manager/logs', currentApi, $scope.logs ? $scope.logs.filters : null); + const csvGenerator = new CsvGenerator(output.csv, 'logs.csv'); + csvGenerator.download(true); + } catch (error) { + errorHandler.handle(error,'Download CSV'); + if(!$rootScope.$$phase) $rootScope.$digest(); + } + } + const initialize = async () => { - try{ + try{ await $scope.logs.nextPage(); const data = await apiReq.request('GET', '/manager/logs/summary', {}); $scope.summary = data.data.data; diff --git a/public/controllers/ruleset.js b/public/controllers/ruleset.js index ec539cc230..4f4eacb20a 100644 --- a/public/controllers/ruleset.js +++ b/public/controllers/ruleset.js @@ -10,10 +10,11 @@ * Find more information about this on the LICENSE file. */ import * as modules from 'ui/modules' +import CsvGenerator from './csv-generator' const app = modules.get('app/wazuh', []); -app.controller('rulesController', function ($scope, $rootScope, Rules, RulesRelated, RulesAutoComplete, errorHandler, genericReq, appState) { +app.controller('rulesController', function ($scope, $rootScope, Rules, RulesRelated, RulesAutoComplete, errorHandler, genericReq, appState, csvReq) { $scope.setRulesTab = tab => $rootScope.globalsubmenuNavItem2 = tab; @@ -66,6 +67,18 @@ app.controller('rulesController', function ($scope, $rootScope, Rules, RulesRela } }; + $scope.downloadCsv = async () => { + try { + const currentApi = JSON.parse(appState.getCurrentAPI()).id; + const output = await csvReq.fetch('/rules', currentApi, $scope.rules ? $scope.rules.filters : null); + const csvGenerator = new CsvGenerator(output.csv, 'rules.csv'); + csvGenerator.download(true); + } catch (error) { + errorHandler.handle(error,'Download CSV'); + if(!$rootScope.$$phase) $rootScope.$digest(); + } + } + /** * This function takes back to the list but adding a group filter */ @@ -169,7 +182,7 @@ app.controller('rulesController', function ($scope, $rootScope, Rules, RulesRela }); }); -app.controller('decodersController', function ($scope, $rootScope, $sce, Decoders, DecodersRelated, DecodersAutoComplete, errorHandler, genericReq, appState) { +app.controller('decodersController', function ($scope, $rootScope, $sce, Decoders, DecodersRelated, DecodersAutoComplete, errorHandler, genericReq, appState, csvReq) { $scope.setRulesTab = tab => $rootScope.globalsubmenuNavItem2 = tab; //Initialization @@ -257,6 +270,18 @@ app.controller('decodersController', function ($scope, $rootScope, $sce, Decoder } } + $scope.downloadCsv = async () => { + try { + const currentApi = JSON.parse(appState.getCurrentAPI()).id; + const output = await csvReq.fetch('/decoders', currentApi, $scope.decoders ? $scope.decoders.filters : null); + const csvGenerator = new CsvGenerator(output.csv, 'decoders.csv'); + csvGenerator.download(true); + } catch (error) { + errorHandler.handle(error,'Download CSV'); + if(!$rootScope.$$phase) $rootScope.$digest(); + } + } + /** * This function changes to the decoder detail view */ diff --git a/public/services/csv-request.js b/public/services/csv-request.js new file mode 100644 index 0000000000..43db0c850e --- /dev/null +++ b/public/services/csv-request.js @@ -0,0 +1,27 @@ +/* + * Wazuh app - API request service + * Copyright (C) 2018 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 * as modules from 'ui/modules' + +const app = modules.get('app/wazuh', []); + +app.service('csvReq', function (genericReq) { + return { + fetch: async (path, id, filters = null) => { + try { + const output = await genericReq.request('POST','/api/wazuh-api/csv',{ path, id, filters }); + return output.data; + } catch (error) { + return Promise.reject(error); + } + } + } +}); \ No newline at end of file diff --git a/public/services/error-handler.js b/public/services/error-handler.js index da809bd181..df05746c6a 100644 --- a/public/services/error-handler.js +++ b/public/services/error-handler.js @@ -20,6 +20,7 @@ app.service('errorHandler', function ( Notifier, appState, $location) { if(error.data && error.data.errorData && error.data.errorData.message) return error.data.errorData.message; if(error.errorData && error.errorData.message) return error.errorData.message; if(error.data && typeof error.data === 'string') return error.data; + if(error.data && error.data.error && typeof error.data.error === 'string') return error.data.error; if(error.data && error.data.message && typeof error.data.message === 'string') return error.data.message; if(error.data && error.data.message && error.data.message.msg && typeof error.data.message.msg === 'string') return error.data.message.msg; if(error.data && error.data.data && typeof error.data.data === 'string') return error.data.data; @@ -119,7 +120,7 @@ app.service('errorHandler', function ( Notifier, appState, $location) { if(error.extraMessage) text = error.extraMessage; text = location ? location + '. ' + text : text; if(!silent){ - if(isWarning) notify.warning(text); + if(isWarning || (text && typeof text === 'string' && text.toLowerCase().includes('no results'))) notify.warning(text); else notify.error(text); } if(goSettings) $location.path('/settings'); diff --git a/public/templates/agents-prev/agents-prev.html b/public/templates/agents-prev/agents-prev.html index b327adc397..75aef324ec 100644 --- a/public/templates/agents-prev/agents-prev.html +++ b/public/templates/agents-prev/agents-prev.html @@ -109,4 +109,8 @@ class="no-lateral-padding" full="'agent'"> +
+ + Formatted +
diff --git a/public/templates/manager/groups/groups-preview.html b/public/templates/manager/groups/groups-preview.html index 3d9ec6486a..9ea4578a3d 100644 --- a/public/templates/manager/groups/groups-preview.html +++ b/public/templates/manager/groups/groups-preview.html @@ -52,9 +52,12 @@ {name:'MD5 agent.conf'} ]"> - - + + +
+ + Formatted +
+ + + +
+ + Formatted +
+ - - - +
+ + Formatted +
+
diff --git a/public/templates/manager/manager-osseclog.html b/public/templates/manager/manager-osseclog.html index 9f4aa3055e..c1ec81c5f2 100644 --- a/public/templates/manager/manager-osseclog.html +++ b/public/templates/manager/manager-osseclog.html @@ -18,6 +18,7 @@
+ @@ -83,5 +84,9 @@ nopointer="true" noheight="true"> +
+ + Formatted +
diff --git a/public/templates/manager/ruleset/decoders/decoders-list.html b/public/templates/manager/ruleset/decoders/decoders-list.html index 7f2aa64529..a9118e7306 100644 --- a/public/templates/manager/ruleset/decoders/decoders-list.html +++ b/public/templates/manager/ruleset/decoders/decoders-list.html @@ -83,4 +83,8 @@ isdecoders="true" class="no-lateral-padding"> +
+ + Formatted +
diff --git a/public/templates/manager/ruleset/rules/rules-list.html b/public/templates/manager/ruleset/rules/rules-list.html index ee78c55b48..d99ba162b0 100644 --- a/public/templates/manager/ruleset/rules/rules-list.html +++ b/public/templates/manager/ruleset/rules/rules-list.html @@ -103,4 +103,8 @@ isdecoders="false" class="no-lateral-padding"> +
+ + Formatted +
diff --git a/server/controllers/wazuh-api.js b/server/controllers/wazuh-api.js index e8bac60243..bf0d30eb8f 100644 --- a/server/controllers/wazuh-api.js +++ b/server/controllers/wazuh-api.js @@ -21,7 +21,7 @@ import ElasticWrapper from '../lib/elastic-wrapper' import getPath from '../../util/get-path' import packageInfo from '../../package.json' import monitoring from '../monitoring' - +import { Parser } from 'json2csv'; const blueWazuh = colors.blue('wazuh'); export default class WazuhApi { @@ -497,4 +497,69 @@ export default class WazuhApi { return reply(this.genericErrorBuilder(500,6,error.message || error)).code(500) } } + + /** + * Get full data on CSV format from a list Wazuh API endpoint + * @param {*} req + * @param {*} res + */ + async csv(req,res) { + try{ + + if(!req.payload || !req.payload.path) throw new Error('Field path is required') + if(!req.payload.id) throw new Error('Field id is required') + + const filters = req.payload && req.payload.filters && Array.isArray(req.payload.filters) ? + req.payload.filters : + []; + + const config = await this.wzWrapper.getWazuhConfigurationById(req.payload.id) + + let path = req.payload.path; + + if(path && typeof path === 'string'){ + path = path[0] === '/' ? path.substr(1) : path + } + + if(!path) throw new Error('An error occurred parsing path field') + + const params = { limit: 99999 }; + + if(filters.length) { + for(const filter of filters) { + if(!filter.name || !filter.value) continue; + params[filter.name] = filter.value; + } + } + + const output = await needle('get', `${config.url}:${config.port}/${path}`, params, { + username : config.user, + password : config.password, + rejectUnauthorized: !config.insecure + }) + + if(output && output.body && output.body.data && output.body.data.totalItems) { + const fields = Object.keys(output.body.data.items[0]); + const data = output.body.data.items; + + const json2csvParser = new Parser({ fields }); + + const csv = json2csvParser.parse(data); + + return res({ csv }); + + } else if (output && output.body && output.body.data && !output.body.data.totalItems) { + + throw new Error('No results') + + } else { + + throw new Error('An error occurred fetching data from the Wazuh API') + + } + + } catch (error) { + return res({ error: error.message || error }).code(500) + } + } } diff --git a/server/routes/wazuh-api.js b/server/routes/wazuh-api.js index 731d7f7fc0..2b9bb81de7 100644 --- a/server/routes/wazuh-api.js +++ b/server/routes/wazuh-api.js @@ -33,12 +33,15 @@ export default (server, options) => { // Write in debug log server.route({ method: 'POST', path: '/api/wazuh/errlog', handler: (req,res) => ctrl.postErrorLog(req,res) }); - // COMMENT HERE + // Force fetch data to be inserted on wazuh-monitoring indices server.route({ method: 'GET', path: '/api/wazuh-api/fetchAgents', handler: (req,res) => ctrl.fetchAgents(req,res) }); - // COMMENT HERE + // Returns the config.yml file parsed server.route({ method: 'GET', path: '/api/wazuh-api/configuration', handler: (req,res) => ctrl.getConfigurationFile(req,res) }); - // COMMENT HERE - server.route({ method: 'POST',path: '/api/wazuh-api/wlogin', handler: (req,res) => ctrl.login(req,res) }); + // Experimental feature to simulate a login system + server.route({ method: 'POST', path: '/api/wazuh-api/wlogin', handler: (req,res) => ctrl.login(req,res) }); + + // Returns data from the Wazuh API on CSV readable format + server.route({ method: 'POST', path: '/api/wazuh-api/csv', handler: (req,res) => ctrl.csv(req,res)}) };