module.exports = { friendlyName: 'Download vulnerabilities csv', description: 'Download vulnerabilities csv file (returning a stream).', inputs: { minSeverity: { description: 'Optional filter to only get vulnerabilities whose `severity` is >= the specified value.', type: 'number', defaultsTo: 0, }, maxSeverity: { description: 'Optional filter to only get vulnerabilities whose `severity` is <= the specified value.', type: 'number', defaultsTo: 10, }, sortBy: { description: 'An optional facet to sort vulnerabilities by.', type: 'string', isIn: [ 'cveId', 'severity', 'hasKnownExploit', 'publishedAt', 'resolvedAt', ], defaultsTo: 'publishedAt' }, sortDirection: { type: 'string', isIn: [ 'ASC', 'DESC', ], defaultsTo: 'DESC' }, page: { description: 'The zero-indexed page number.', type: 'number', defaultsTo: 0 }, teamApid: { description: 'The ID of the Team to filter by, or 0 to only include hosts with no team, or undefined to not filter by any team.', type: 'number', }, pageSize: { description: 'The number of vulnerabilities to export', type: 'number', defaultsTo: 9999, }, exportType: { description: 'The type of CSV export this will be.', type: 'string', isIn: [ 'resolvedAndVulnerableInstalls', 'overview' ], }, }, exits: { success: { outputFriendlyName: 'File', outputDescription: 'The streaming bytes of the file.', outputType: 'ref' }, emptyExport: { description: 'No matching vulnerabilities could be found with the provided filters', responseType: 'notFound', } }, fn: async function ({minSeverity, maxSeverity, sortBy, sortDirection, page, pageSize, teamApid, exportType}) { // Generate a random room name. let roomId = await sails.helpers.strings.random(); if(this.req.isSocket) { // Add the requesting socket to the room. sails.sockets.join(this.req, roomId); } let includeResolvedInstalls = false; // If the export type is resolvedAndVulnerableInstalls, we'll ask the get-vulnerabilities helper to include resolved install information in the affectedInstalls array. if(exportType === 'resolvedAndVulnerableInstalls'){ includeResolvedInstalls = true; } // Get a report from the vulnerability helper let report = await sails.helpers.getVulnerabilities.with({minSeverity, maxSeverity, sortBy, sortDirection, page, pageSize, teamApid, includeResolvedInstalls}) .intercept('noMatchingVulnerabilities', ()=>{ return 'emptyExport'; }); let stream = require('stream'); let csvString = ''; // Create a writeable stream we'll use to create the csvString. let writableStream = new stream.Writable({//[?]: https://nodejs.org/api/stream.html#writable_writechunk-encoding-callback write(chunk, encoding, callback) { csvString += chunk.toString(); callback(); } }); let csv = require('fast-csv'); let generatingCsv = csv.format({headers: true}); // Pass the writableStream into generatingCsv. generatingCsv.pipe(writableStream);// [?]: https://c2fo.github.io/fast-csv/docs/formatting/methods#write // Now build the CSV report. for (let vulnerability of report.entries) { // If the export type is overview, we'll add a row for every CVE in the report. if (exportType === 'overview') { let csvRowForThisVulnerability = {}; csvRowForThisVulnerability['CVE ID'] = vulnerability.cveId; csvRowForThisVulnerability['Severity'] = vulnerability.severity; csvRowForThisVulnerability['Has known exploit'] = !!vulnerability.hasKnownExploit; csvRowForThisVulnerability['CVE description'] = vulnerability.cveDescription ? vulnerability.cveDescription : 'N/A'; csvRowForThisVulnerability['Publish date'] = new Date(vulnerability.publishedAt); csvRowForThisVulnerability['Resolved Date'] = vulnerability.resolvedAt !== 0 ? new Date(vulnerability.resolvedAt) : 'N/A'; csvRowForThisVulnerability['Number of affected hosts'] = vulnerability.numAffectedHosts; generatingCsv.write(csvRowForThisVulnerability); } else if (exportType === 'resolvedAndVulnerableInstalls') { // If the export type is resolvedAndVulnerableInstalls, we'll add a row for every software install included the report. // To do this, we'll iterate through the hosts affected by this vulnerability, and add a row for each vulnerable install record that is affected by this CVE on this host. for (let host of vulnerability.affectedHosts) { let installsForThisHost = vulnerability.affectedInstalls.filter((install) => { return install.affectedHost === host.id; }); for (let install of installsForThisHost) { let csvRowForThisInstall = {}; csvRowForThisInstall['CVE ID'] = vulnerability.cveId; csvRowForThisInstall['Severity'] = vulnerability.severity; csvRowForThisInstall['Has known exploit'] = !!vulnerability.hasKnownExploit; csvRowForThisInstall['CVE description'] = vulnerability.cveDescription ? vulnerability.cveDescription : 'N/A'; csvRowForThisInstall['Publish date'] = new Date(vulnerability.publishedAt); csvRowForThisInstall['Resolved Date'] = install.uninstalledAt !== 0 ? new Date(install.uninstalledAt) : 'N/A'; csvRowForThisInstall['Affected software name'] = install.name; csvRowForThisInstall['Affected software version'] = install.version; csvRowForThisInstall['Resolved in version'] = install.resolvedInVersion ? install.resolvedInVersion : 'N/A'; csvRowForThisInstall['Affected software URL'] = install.url; csvRowForThisInstall['Vulnerable software detected on'] = new Date(install.installedAt); csvRowForThisInstall['Host Fleet URL'] = sails.config.custom.fleetBaseUrl + '/hosts/' + encodeURIComponent(host.fleetApid); csvRowForThisInstall['Host display name'] = host.displayName; csvRowForThisInstall['Host team'] = host.teamDisplayName; csvRowForThisInstall['Host serial number'] = host.hardwareSerialNumber; csvRowForThisInstall['Host UUID'] = host.uuid; csvRowForThisInstall['Host team ID'] = host.teamApid !== 0 ? host.teamApid : 'N/A'; generatingCsv.write(csvRowForThisInstall); }//∞ }//∞ } }//∞ generatingCsv.end(); // After the the csvString has been generated by the writableStream, broadcast the csvString to the requesting user's socket. writableStream.on('finish', () => { if(this.req.isSocket){ // Note: we're sending the cveId with the cvsString, this is so we can set the filename in our frontend code. sails.sockets.broadcast(roomId, 'csvExportDone', csvString); // Unsubscribe the socket from the room. sails.sockets.leave(this.req, roomId); } else { return csvString; } }); } };