mirror of
https://github.com/fleetdm/fleet
synced 2026-05-04 13:59:01 +00:00
Closes: https://github.com/fleetdm/confidential/issues/4057 Changes: - Added the contents of the fleet-vulnerability-dashboard repo to ee/vulnerability-dashboard - Added a github workflow to deploy the vulnerability dashboard on Heroku - Added a github workflow to test changes to the vulnerability-dashboard - Updated the website's custom configuration to enable auto-approvals/review requests to files in the ee/vulnerability-dashboard folder
182 lines
7.1 KiB
JavaScript
182 lines
7.1 KiB
JavaScript
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
};
|