fleet/ee/vulnerability-dashboard/api/controllers/dashboard/view-welcome.js
Eric 58e62ba75f
Vulnerability dashboard: update priority patch progress calculation. (#21348)
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.
2024-08-15 13:02:45 -05:00

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,
}
};
}
};