mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Changes: - Optimized patch progress calculation - Moved patch progress calculation to a new action `get-priority-vulnerabilities` that is called after the dashboard page laods - Added a loading state to the patch progress section of the dashboard page.
389 lines
19 KiB
JavaScript
389 lines
19 KiB
JavaScript
module.exports = {
|
|
|
|
|
|
friendlyName: 'View welcome page',
|
|
|
|
|
|
description: 'Display the dashboard "Welcome" page.',
|
|
|
|
|
|
exits: {
|
|
|
|
success: {
|
|
viewTemplatePath: 'pages/dashboard/welcome',
|
|
description: 'Display the welcome page for authenticated users.'
|
|
},
|
|
|
|
},
|
|
|
|
|
|
fn: async function () {
|
|
|
|
let realDataForGraphs = {};
|
|
|
|
// ┌┐ ┬ ┬┬┬ ┌┬┐ ┌┐┌┌─┐┌┬┐┬┬ ┬┌─┐ ┌─┐ ┬ ┬┌─┐┬─┐┬┌─┐┌─┐
|
|
// ├┴┐│ │││ ││ │││├─┤ │ │└┐┌┘├┤ │─┼┐│ │├┤ ├┬┘│├┤ └─┐
|
|
// └─┘└─┘┴┴─┘─┴┘ ┘└┘┴ ┴ ┴ ┴ └┘ └─┘ └─┘└└─┘└─┘┴└─┴└─┘└─┘
|
|
|
|
// Create a SQL query to find Vulnerability records where all associated VulnerabilityInstall records have a non-zero uninstalledAt timestamp.
|
|
// Note: this query uses MYSQL specific syntax.
|
|
let resolvedVulnerabilitiesWithResolutionTimestampQuery = `
|
|
SELECT
|
|
vulnerability.id,
|
|
vulnerability.cveId,
|
|
vulnerability.fleetSoftwareItemUrl,
|
|
vulnerability.additionalDetailsUrl,
|
|
vulnerability.probabilityOfExploit,
|
|
vulnerability.severity,
|
|
vulnerability.hasKnownExploit,
|
|
vulnerability.publishedAt,
|
|
CASE
|
|
WHEN COUNT(vulnerabilityinstall.id) = 0 THEN NULL
|
|
WHEN MIN(vulnerabilityinstall.uninstalledAt) = 0 THEN NULL
|
|
ELSE MAX(vulnerabilityinstall.uninstalledAt)
|
|
END AS resolvedAt
|
|
FROM
|
|
vulnerability
|
|
LEFT JOIN
|
|
vulnerabilityinstall ON vulnerability.id = vulnerabilityinstall.vulnerability
|
|
GROUP BY
|
|
vulnerability.id, vulnerability.cveId, vulnerability.fleetSoftwareItemUrl, vulnerability.additionalDetailsUrl, vulnerability.probabilityOfExploit, vulnerability.severity, vulnerability.hasKnownExploit, vulnerability.publishedAt
|
|
HAVING
|
|
(COUNT(vulnerabilityinstall.id) = 0 OR MIN(vulnerabilityinstall.uninstalledAt) > 0);`;
|
|
|
|
// Create two more SQL queries, the first finds distinct hosts that are currently affected by a critical severity vulnerability, the second finds distinct hosts that are currently affected by a high severity vulnerability.
|
|
let hostsWithCriticalVulnsQuery = `
|
|
SELECT DISTINCT
|
|
Host.*
|
|
FROM
|
|
Host
|
|
WHERE
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM VulnerabilityInstall
|
|
INNER JOIN Vulnerability ON VulnerabilityInstall.vulnerability = Vulnerability.id
|
|
WHERE
|
|
VulnerabilityInstall.host = Host.id
|
|
AND Vulnerability.severity >= 9
|
|
AND VulnerabilityInstall.uninstalledat = 0
|
|
);`;
|
|
let hostsWithHighVulnsQuery = `
|
|
SELECT DISTINCT
|
|
Host.*
|
|
FROM
|
|
Host
|
|
WHERE
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM VulnerabilityInstall
|
|
INNER JOIN Vulnerability ON VulnerabilityInstall.vulnerability = Vulnerability.id
|
|
WHERE
|
|
VulnerabilityInstall.host = Host.id
|
|
AND Vulnerability.severity < 9
|
|
AND Vulnerability.severity >= 7
|
|
AND VulnerabilityInstall.uninstalledat = 0
|
|
);`;
|
|
|
|
// If this app is configured to use a Postgres DB, we'll change the native SQL queries to be compatible.
|
|
if(sails.config.datastores.default.adapter === 'sails-postgresql') {
|
|
resolvedVulnerabilitiesWithResolutionTimestampQuery = `
|
|
SELECT
|
|
"vulnerability".*,
|
|
CASE
|
|
WHEN COUNT("vulnerabilityinstall".id) = 0 THEN 0
|
|
WHEN MIN("vulnerabilityinstall"."uninstalledAt") = 0 THEN 0
|
|
ELSE MAX("vulnerabilityinstall"."uninstalledAt")
|
|
END AS "resolvedAt"
|
|
FROM
|
|
vulnerability
|
|
LEFT JOIN
|
|
vulnerabilityinstall ON vulnerability.id = vulnerabilityinstall."vulnerability"
|
|
GROUP BY
|
|
vulnerability.id, vulnerability."cveId", vulnerability."fleetSoftwareItemUrl", vulnerability."additionalDetailsUrl", vulnerability."probabilityOfExploit", vulnerability."severity", vulnerability."hasKnownExploit", vulnerability."publishedAt"
|
|
HAVING
|
|
(COUNT(vulnerabilityinstall.id) = 0 OR MIN(vulnerabilityinstall."uninstalledAt") > 0);`;
|
|
|
|
hostsWithCriticalVulnsQuery = `
|
|
SELECT DISTINCT
|
|
Host.*
|
|
FROM
|
|
Host
|
|
WHERE
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM VulnerabilityInstall
|
|
INNER JOIN Vulnerability ON VulnerabilityInstall.vulnerability = Vulnerability.id
|
|
WHERE
|
|
VulnerabilityInstall.host = Host.id
|
|
AND Vulnerability.severity >= 9
|
|
AND VulnerabilityInstall."uninstalledAt" = 0
|
|
);`;
|
|
hostsWithHighVulnsQuery = `
|
|
SELECT DISTINCT
|
|
Host.*
|
|
FROM
|
|
Host
|
|
WHERE
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM VulnerabilityInstall
|
|
INNER JOIN Vulnerability ON VulnerabilityInstall.vulnerability = Vulnerability.id
|
|
WHERE
|
|
VulnerabilityInstall.host = Host.id
|
|
AND Vulnerability.severity < 9
|
|
AND Vulnerability.severity >= 7
|
|
AND VulnerabilityInstall."uninstalledAt" = 0
|
|
);`;
|
|
}
|
|
// Performance notes:
|
|
// Hosted Postgres DB (4,989 Vulnerabilities, 12,232 hosts, and 395,728 Installs):
|
|
// - Waterline queries (all hosts w/ vulnerabilities populated & all Vulnerabilities w/ installs populated): 56.9s
|
|
// - Native SQL queries: 1.4s
|
|
// Local MYSQL DB (5,719 Vulnerabilities, 8,159 hosts, and 819,158 Installs):
|
|
// - Waterline queries (all hosts w/ vulnerabilities populated & all Vulnerabilities w/ installs populated): 24.9s
|
|
// - Native SQL queries: 4.1s
|
|
|
|
// ┌─┐┌─┐┌┬┐┬ ┬┌─┐┬─┐ ┬┌┐┌┌─┐┌─┐┬─┐┌┬┐┌─┐┌┬┐┬┌─┐┌┐┌ ┌─┐┬─┐┌─┐┌┬┐ ┌┬┐┌─┐┌┬┐┌─┐┌┐ ┌─┐┌─┐┌─┐
|
|
// │ ┬├─┤ │ ├─┤├┤ ├┬┘ ││││├┤ │ │├┬┘│││├─┤ │ ││ ││││ ├┤ ├┬┘│ ││││ ││├─┤ │ ├─┤├┴┐├─┤└─┐├┤
|
|
// └─┘┴ ┴ ┴ ┴ ┴└─┘┴└─ ┴┘└┘└ └─┘┴└─┴ ┴┴ ┴ ┴ ┴└─┘┘└┘ └ ┴└─└─┘┴ ┴ ─┴┘┴ ┴ ┴ ┴ ┴└─┘┴ ┴└─┘└─┘
|
|
|
|
// console.time('native queries');
|
|
let rawResultForResolvedVulnerabilities = await sails.sendNativeQuery(resolvedVulnerabilitiesWithResolutionTimestampQuery);
|
|
let rawResultForHostsWithHighVulns = await sails.sendNativeQuery(hostsWithHighVulnsQuery);
|
|
let rawResultForHostsWithCriticalVulns = await sails.sendNativeQuery(hostsWithCriticalVulnsQuery);
|
|
// console.timeEnd('native queries');
|
|
|
|
// console.time('waterline queries')
|
|
// let Tesvulnerabilities = await Vulnerability.find().populate('installs');
|
|
// let Teshosts = await Host.find().populate('vulnerabilities');
|
|
// console.timeEnd('waterline queries')
|
|
|
|
let resolvedVulnerabilities = rawResultForResolvedVulnerabilities.rows;
|
|
let hostsWithHighVulnerabilities = rawResultForHostsWithHighVulns.rows;
|
|
let hostsWithCriticalVulnerabilities = rawResultForHostsWithCriticalVulns.rows;
|
|
|
|
// Get all vulnerabilities, we'll use these to track the newly detected vulnerabilities and the total number of vulnerabilities by severity over time.
|
|
let vulnerabilities = await Vulnerability.find();
|
|
|
|
// Get the total number of hosts enrolled the fleet instance. This will be used as the denominiator in the Percentage of hosts with vulnerabiilities by severity graph.
|
|
let totalNumberOfHosts = await Host.count();
|
|
|
|
|
|
// * * * * * quick sanity check to make sure the results from native queries are the same as previous results * * * * *
|
|
// let criticalVulnerabilities = await Vulnerability.find({severity: {'>=': 9}}).populate('hosts').populate('installs');
|
|
|
|
// let currentCriticalVulnerabilities = criticalVulnerabilities.filter((vulnerability)=>{
|
|
// let vulnIsCurrentlyInstalled = _.some(vulnerability.installs, (install)=>{
|
|
// return install.uninstalledAt === 0;
|
|
// });
|
|
// return vulnIsCurrentlyInstalled;
|
|
// });
|
|
// let allAffectedInstallsFromCurrentVulnerabilities = _.pluck(currentCriticalVulnerabilities, 'installs')
|
|
// let affectedCriticalInstalls = [];
|
|
// for(let installs of allAffectedInstallsFromCurrentVulnerabilities) {
|
|
// let hostsAffectedByThisInstall = installs.filter((install)=>{
|
|
// return install.uninstalledAt === 0
|
|
// });
|
|
// affectedCriticalInstalls = affectedCriticalInstalls.concat(hostsAffectedByThisInstall);
|
|
// }
|
|
// let allHostsAffectedByCriticalVulnerabilities = [];
|
|
// for(let vuln of currentCriticalVulnerabilities){
|
|
// allHostsAffectedByCriticalVulnerabilities = allHostsAffectedByCriticalVulnerabilities.concat(vuln.hosts)
|
|
// };
|
|
// allUniqueHostDisplayNamesAffectedByCriticalVulnerabilities = _.uniq(allHostsAffectedByCriticalVulnerabilities, 'id');
|
|
// // console.log(allUniqueHostDisplayNamesAffectedByCriticalVulnerabilities);
|
|
// let differenceBetweenMethods = (_.pluck(rawResultForHostsWithCriticalVulns.rows, 'id').length === _.pluck(allUniqueHostDisplayNamesAffectedByCriticalVulnerabilities, 'id').length);
|
|
// if(!differenceBetweenMethods){
|
|
// console.log('number of hosts with critical vulns (from nativequery): ',rawResultForHostsWithCriticalVulns.rows.length)
|
|
// console.log('Number of hosts with critical vulns (from sanity check): ',allUniqueHostDisplayNamesAffectedByCriticalVulnerabilities.length)
|
|
// console.log('number of critical vulns: ',criticalVulnerabilities.length);
|
|
// console.log('number of current critical vulns: ',currentCriticalVulnerabilities.length);
|
|
// throw new Error('The native query returned a different number of results');
|
|
// }
|
|
// // * * *
|
|
|
|
// ┌┐ ┬ ┬┬┬ ┌┬┐ ┌┬┐┌─┐┌┬┐┌─┐┌─┐┌─┐┌┬┐┌─┐ ┌─┐┌─┐┬─┐ ┌─┐┬─┐┌─┐┌─┐┬ ┬┌─┐
|
|
// ├┴┐│ │││ ││ ││├─┤ │ ├─┤└─┐├┤ │ └─┐ ├┤ │ │├┬┘ │ ┬├┬┘├─┤├─┘├─┤└─┐
|
|
// └─┘└─┘┴┴─┘─┴┘ ─┴┘┴ ┴ ┴ ┴ ┴└─┘└─┘ ┴ └─┘ └ └─┘┴└─ └─┘┴└─┴ ┴┴ ┴ ┴└─┘
|
|
|
|
// Create a filtered array of all critical vulnerabilities with no unresolved vulnerability installs.
|
|
let resolvedCriticalVulnerabilities = [];
|
|
|
|
for(let vuln of resolvedVulnerabilities){
|
|
// If the vulnerability has a lower than critical severity, we'll ignore it.
|
|
if(vuln.severity < 9){
|
|
continue;
|
|
}
|
|
// Add information about this resolved critical vulnerability to the resolvedCriticalVulnerabilities array.
|
|
let vulnerability = {
|
|
cveId: vuln.cveId,
|
|
resolvedAt: vuln.resolvedAt,
|
|
daysToFullyResolve: ( vuln.resolvedAt - vuln.createdAt) / (24 * 60 * 60 * 1000)
|
|
};
|
|
resolvedCriticalVulnerabilities.push(vulnerability);
|
|
}//∞
|
|
|
|
let newPublishedVulns = vulnerabilities.filter((vuln)=>{
|
|
return vuln.createdAt > Date.now() - (1000 * 60 * 60 * 48);
|
|
});
|
|
|
|
let newPublishedCriticalVulns = newPublishedVulns.filter((vuln)=>{
|
|
return vuln.severity >= 9;
|
|
});
|
|
|
|
let newPublishedHighVulns = newPublishedVulns.filter((vuln)=>{
|
|
return vuln.severity < 9 && vuln.severity >= 7;
|
|
});
|
|
|
|
realDataForGraphs.newPublishedVulnerabilities = {
|
|
totalNumberOfNewVulnerabilities: newPublishedVulns.length > 0 ? newPublishedVulns.length : 0,
|
|
numberOfNewCriticalVulnerabilities: newPublishedCriticalVulns.length > 0 ? newPublishedCriticalVulns.length : 0,
|
|
numberOfNewHighVulnerabilities: newPublishedHighVulns.length > 0 ? newPublishedHighVulns.length : 0,
|
|
};
|
|
|
|
let percentageOfHostsWithHighVuln = Math.round((hostsWithHighVulnerabilities.length / totalNumberOfHosts * 100).toFixed(2));
|
|
let percentageOfHostsWithNoHighVuln = Math.round(100 - percentageOfHostsWithHighVuln);
|
|
let percentageOfHostsWithCriticalVuln = Math.round((hostsWithCriticalVulnerabilities.length / totalNumberOfHosts * 100).toFixed(2));
|
|
let percentageOfHostsWithNoCriticalVuln = Math.round(100 - percentageOfHostsWithCriticalVuln);
|
|
|
|
realDataForGraphs.criticalVulnerabilityPercentage = [percentageOfHostsWithCriticalVuln, percentageOfHostsWithNoCriticalVuln];
|
|
realDataForGraphs.highVulnerabilityPercentage = [percentageOfHostsWithHighVuln, percentageOfHostsWithNoHighVuln];
|
|
|
|
|
|
let criticalSeverity = vulnerabilities.filter((vuln)=>{
|
|
return vuln.severity >= 9;
|
|
});
|
|
let highSeverity = vulnerabilities.filter((vuln)=>{
|
|
return vuln.severity <= 8.9 && vuln.severity >= 7.0;
|
|
});
|
|
let mediumSeverity = vulnerabilities.filter((vuln)=>{
|
|
return vuln.severity <= 6.9 && vuln.severity >= 4.0;
|
|
});
|
|
let lowSeverity = vulnerabilities.filter((vuln)=>{
|
|
return vuln.severity >= 0 && vuln.severity <= 3.9;
|
|
});
|
|
let unknownSeverity = vulnerabilities.filter((vuln)=>{
|
|
return vuln.severity === 0;
|
|
});
|
|
|
|
realDataForGraphs.numberOfVulnsBySeverity = [criticalSeverity.length, highSeverity.length, mediumSeverity.length, lowSeverity.length, unknownSeverity.length];
|
|
|
|
let timelineDatasets = {
|
|
critical: [],
|
|
high: [],
|
|
medium: [],
|
|
low: [],
|
|
unknown: [],
|
|
};
|
|
|
|
|
|
// Get a timestamp of midnight (UTC) previous the sunday.
|
|
let today = new Date();
|
|
let lastSunday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - today.getDay());
|
|
let lastSundayAt = Date.UTC(lastSunday.getFullYear(), lastSunday.getMonth(), lastSunday.getDate());
|
|
let NUMBER_OF_WEEKS_TO_BUILD_TIMELINE_FOR = 3;
|
|
|
|
// Create a JS timestamp of the sunday three weeks prior to our previous timestamp.
|
|
let threeWeeksFromLastSundayAt = lastSundayAt - (NUMBER_OF_WEEKS_TO_BUILD_TIMELINE_FOR * 7 * 24 * 60 * 60 * 1000);
|
|
|
|
let remediationDataset = [];
|
|
|
|
for(let i = 0; i <= NUMBER_OF_WEEKS_TO_BUILD_TIMELINE_FOR; i++) {
|
|
let weekToAdd = (i * (7 * 24 * 60 * 60 * 1000));
|
|
// Create a JS timestamp for the start and end timestamps we're checking for this iteration.
|
|
let weekStartsAt = threeWeeksFromLastSundayAt + weekToAdd;
|
|
let weekEndsAt = threeWeeksFromLastSundayAt + weekToAdd + (7 * 24 * 60 * 60 * 1000);
|
|
// Filter the array of fully resolved vulnerabilities to find vulnerabilities that were resolved during this time.
|
|
let criticalVulnerabilitiesFullyResolvedDuringThisTime = resolvedCriticalVulnerabilities.filter((resolvedVulnerability)=>{
|
|
return resolvedVulnerability.resolvedAt > weekStartsAt && resolvedVulnerability.resolvedAt < weekEndsAt;
|
|
});
|
|
// console.log(weekStartsAt+' - '+weekEndsAt,criticalVulnerabilitiesFullyResolvedDuringThisTime)
|
|
|
|
// Find the average number of days that it took to fully resolve the vulnerabilities that were resolved during this week.
|
|
let averageNumberOfCriticalVulnerabilitiesResolved = Math.round(_.sum(criticalVulnerabilitiesFullyResolvedDuringThisTime, 'daysToFullyResolve') / criticalVulnerabilitiesFullyResolvedDuringThisTime.length);
|
|
// console.log(criticalVulnerabilitiesFullyResolvedDuringThisTime.length, averageNumberOfCriticalVulnerabilitiesResolved);
|
|
// Create a datapoint for the remediation timeline that has the average number of vulnerabilities resolved, and a timestamp of the start of the week.
|
|
let remediationDatapoint = {
|
|
x: weekStartsAt,
|
|
y: averageNumberOfCriticalVulnerabilitiesResolved > 0 ? averageNumberOfCriticalVulnerabilitiesResolved : 'N/A',
|
|
};
|
|
remediationDataset.push(remediationDatapoint);
|
|
|
|
// Create a filtered array of all critical vulnerabilities that affected hosts during this time.
|
|
let criticalVulnerabilitiesThisWeek = _.filter(criticalSeverity, (vulnerability)=>{
|
|
return vulnerability.createdAt < weekStartsAt;
|
|
});
|
|
// Create a dataset for the graph for this week.
|
|
let criticalDatasetForThisWeek = {
|
|
x: weekStartsAt,
|
|
y: criticalVulnerabilitiesThisWeek.length
|
|
};
|
|
timelineDatasets.critical.push(criticalDatasetForThisWeek);
|
|
// High severity
|
|
let numberOfHighVulnerabilitiesThisWeek = _.filter(highSeverity, (vulnerability)=>{
|
|
return vulnerability.createdAt < weekStartsAt;
|
|
});
|
|
let highDatasetForThisWeek = {
|
|
x: weekStartsAt,
|
|
y: numberOfHighVulnerabilitiesThisWeek.length
|
|
};
|
|
timelineDatasets.high.push(highDatasetForThisWeek);
|
|
// Medium severity
|
|
let numberOfMediumVulnerabilitiesThisWeek = _.filter(mediumSeverity, (vulnerability)=>{
|
|
return vulnerability.createdAt < weekStartsAt;
|
|
});
|
|
let mediumDatasetForThisWeek = {
|
|
x: weekStartsAt,
|
|
y: numberOfMediumVulnerabilitiesThisWeek.length
|
|
};
|
|
timelineDatasets.medium.push(mediumDatasetForThisWeek);
|
|
// Low severity
|
|
let numberOfLowVulnerabilitiesThisWeek = _.filter(lowSeverity, (vulnerability)=>{
|
|
return vulnerability.createdAt < weekStartsAt;
|
|
});
|
|
let lowDatasetForThisWeek = {
|
|
x: weekStartsAt,
|
|
y: numberOfLowVulnerabilitiesThisWeek.length
|
|
};
|
|
timelineDatasets.low.push(lowDatasetForThisWeek);
|
|
// Unknown severity
|
|
let numberOfUnknownVulnerabilitiesThisWeek = _.filter(unknownSeverity, (vulnerability)=>{
|
|
return vulnerability.createdAt < weekStartsAt;
|
|
});
|
|
let unknownDatasetForThisWeek = {
|
|
x: weekStartsAt,
|
|
y: numberOfUnknownVulnerabilitiesThisWeek.length
|
|
};
|
|
timelineDatasets.unknown.push(unknownDatasetForThisWeek);
|
|
}//∞
|
|
|
|
// Add data points for the current state of the vulnerability timeline
|
|
timelineDatasets.critical.push({x: Date.now(), y: criticalSeverity.length});
|
|
timelineDatasets.high.push({x: Date.now(), y: highSeverity.length});
|
|
timelineDatasets.medium.push({x: Date.now(), y: mediumSeverity.length});
|
|
timelineDatasets.low.push({x: Date.now(), y: lowSeverity.length});
|
|
timelineDatasets.unknown.push({x: Date.now(), y: unknownSeverity.length});
|
|
|
|
realDataForGraphs.timelineDatasets = timelineDatasets;
|
|
realDataForGraphs.remediationTimeline = remediationDataset;
|
|
// console.log(realDataForGraphs);
|
|
|
|
|
|
|
|
|
|
return {
|
|
realDataForGraphs: {
|
|
priorityVulnPatchProgress: [],// This information is gathered after the initial page load.
|
|
remediationTimeline: realDataForGraphs.remediationTimeline,
|
|
timelineDatasets: realDataForGraphs.timelineDatasets,
|
|
newPublishedVulnerabilities:realDataForGraphs.newPublishedVulnerabilities,//last 48 hours
|
|
criticalVulnerabilityPercentage: realDataForGraphs.criticalVulnerabilityPercentage,
|
|
highVulnerabilityPercentage: realDataForGraphs.highVulnerabilityPercentage,
|
|
numberOfVulnsBySeverity: realDataForGraphs.numberOfVulnsBySeverity,
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
|
|
};
|