From eb434fe9e233afc6fc3453ad337b7bca9fa7ab10 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 15 Aug 2024 12:50:57 -0500 Subject: [PATCH 1/2] move priority cve progress calculation to separate action, optimize patch progress calculation --- .../api/controllers/dashboard/view-welcome.js | 65 +------------- .../get-priority-vulnerabilities.js | 84 +++++++++++++++++++ .../update-priority-vulnerabilities.js | 28 +++---- .../assets/js/cloud.setup.js | 2 +- .../assets/js/pages/dashboard/welcome.page.js | 8 +- .../styles/pages/dashboard/welcome.less | 3 + ee/vulnerability-dashboard/config/routes.js | 2 +- .../views/pages/dashboard/welcome.ejs | 15 +++- 8 files changed, 120 insertions(+), 87 deletions(-) create mode 100644 ee/vulnerability-dashboard/api/controllers/get-priority-vulnerabilities.js diff --git a/ee/vulnerability-dashboard/api/controllers/dashboard/view-welcome.js b/ee/vulnerability-dashboard/api/controllers/dashboard/view-welcome.js index dc3d56ba7b36..71069df830fe 100644 --- a/ee/vulnerability-dashboard/api/controllers/dashboard/view-welcome.js +++ b/ee/vulnerability-dashboard/api/controllers/dashboard/view-welcome.js @@ -369,74 +369,11 @@ module.exports = { // console.log(realDataForGraphs); - // ┌─┐┌─┐┌┬┐ ┌─┐┬─┐┬┌─┐┬─┐┬┌┬┐┬ ┬ ┌─┐┬ ┬┌─┐ ┌─┐┬─┐┌─┐┌─┐┬─┐┌─┐┌─┐┌─┐ - // │ ┬├┤ │ ├─┘├┬┘││ │├┬┘│ │ └┬┘ │ └┐┌┘├┤ ├─┘├┬┘│ ││ ┬├┬┘├┤ └─┐└─┐ - // └─┘└─┘ ┴ ┴ ┴└─┴└─┘┴└─┴ ┴ ┴ └─┘ └┘ └─┘ ┴ ┴└─└─┘└─┘┴└─└─┘└─┘└─┘ - - // Get the JSON array of Priority CVE IDs from the platform record. - let platformRecord = await Platform.find({}).limit(1); - let priorityVulnerabilities = platformRecord[0].priorityCveIds; - let priorityVulnerabilitiesThatExistInTheDatabase = await Vulnerability.find({isPriority: true}); - let cveIdsThatDontExistInTheDatabase = _.difference(priorityVulnerabilities, _.pluck(priorityVulnerabilitiesThatExistInTheDatabase, 'cveId')); - let priorityVulnPatchProgress = []; - - // Get patch progress for priority CVE IDs that only exist in the Platform record. - for(let cve of cveIdsThatDontExistInTheDatabase){ - // Trim whitespace from the CVE ID. https://github.com/fleetdm/fleet/issues/14904 - let trimmedCveId = _.trim(cve); - // Check to see if a Vulnerability record has been created for this CVE ID. - let vulnRecordForThisCveExists = await Vulnerability.findOne({cveId: trimmedCveId, isPriority: false}); - if(vulnRecordForThisCveExists){// If we found a Vulnerability record that matches a CVE ID, we'll update it to have `isPriority: true`. - let updatedVulnRecord = await Vulnerability.updateOne({id: vulnRecordForThisCveExists.id}).set({isPriority: true}); - priorityVulnerabilitiesThatExistInTheDatabase.push(updatedVulnRecord); - } else {// Otherwise, we'll add 100% patch progress for this CVE. - let patchProgress = { - cveId: trimmedCveId, - patchProgressPercentage: 100, - additionalDetailsUrl: 'https://nvd.nist.gov/vuln/detail/'+ encodeURIComponent(cve), - }; - priorityVulnPatchProgress.push(patchProgress); - } - } - - // Get patch progress for CVEs we have records for. - for(let vuln of priorityVulnerabilitiesThatExistInTheDatabase) { - let vulnPatchProgress = _.clone(vuln); - vulnPatchProgress.affectedSoftware = []; - // Calculate how many host have been affected by this vulnerability, and how many hosts are currently affected by this vulnerability - let installsForThisVulnerability = await VulnerabilityInstall.find({vulnerability: vuln.id}); - // This number will represent the number of hosts that are currently affected by the vulnerability - let uniqueAffectedHosts = _.uniq(_.pluck(installsForThisVulnerability, 'host')); - - let resolvedInstallsForThisVuln = []; - let affectedSoftwareForThisVulnerability = []; - vulnPatchProgress.numberOfHostsAffected = uniqueAffectedHosts.length; - for(let install of installsForThisVulnerability) { - // If the install has a non-zero uninstalledAt value, then it has been uninstalled. - if(install.uninstalledAt !== 0) { - // If this is a resolved install, we'll check the other installs for this vulnerability to see if there is an unresolved VulnerabilityInstall for this host. - if(!_.find(installsForThisVulnerability, {host: install.host, uninstalledAt: 0})){ - // If an unresolved vulnerabilityInstall record affecting this host is found, we won't count this vulnerability as resolved for this host. - resolvedInstallsForThisVuln.push(install); - } - } - affectedSoftwareForThisVulnerability.push({name: install.softwareName, version: install.versionName, url: sails.config.custom.fleetBaseUrl+'/software/'+install.fleetApid }); - }//∞ - - // Get the number of unique hosts who were previosuly affected by this vulnerability. - let uniqNumberOfResolvedInstallsForThisVuln = _.uniq(resolvedInstallsForThisVuln, 'host').length; - vulnPatchProgress.affectedSoftware = _.uniq(affectedSoftwareForThisVulnerability, 'url'); - // To calculate the patch progress, we'll use the number of unique hosts who were previously affected by this vulnerability as the numerator and the number of unique hosts affected by the vulnerability as the denominator. - vulnPatchProgress.patchProgressPercentage = Math.floor((uniqNumberOfResolvedInstallsForThisVuln / vulnPatchProgress.numberOfHostsAffected) * 100); - priorityVulnPatchProgress.push(vulnPatchProgress); - }//∞ - // Sort the priority vulnerabilities by CVE ID. - priorityVulnPatchProgress = _.sortBy(priorityVulnPatchProgress, 'cveId'); return { realDataForGraphs: { - priorityVulnPatchProgress, + priorityVulnPatchProgress: [],// This information is gathered after the initial page load. remediationTimeline: realDataForGraphs.remediationTimeline, timelineDatasets: realDataForGraphs.timelineDatasets, newPublishedVulnerabilities:realDataForGraphs.newPublishedVulnerabilities,//last 48 hours diff --git a/ee/vulnerability-dashboard/api/controllers/get-priority-vulnerabilities.js b/ee/vulnerability-dashboard/api/controllers/get-priority-vulnerabilities.js new file mode 100644 index 000000000000..96a1ff8ba95e --- /dev/null +++ b/ee/vulnerability-dashboard/api/controllers/get-priority-vulnerabilities.js @@ -0,0 +1,84 @@ +module.exports = { + + + friendlyName: 'Get priority vulnerabilities', + + + description: 'Returns information about priority CVEs.', + extendedDescription: 'This code was previously in the view action for the dashboard page, but was moved to a separate action to reduce inital page laoding time.', + + + inputs: { + + }, + + + exits: { + + }, + + + fn: async function (inputs) { + // ┌─┐┌─┐┌┬┐ ┌─┐┬─┐┬┌─┐┬─┐┬┌┬┐┬ ┬ ┌─┐┬ ┬┌─┐ ┌─┐┬─┐┌─┐┌─┐┬─┐┌─┐┌─┐┌─┐ + // │ ┬├┤ │ ├─┘├┬┘││ │├┬┘│ │ └┬┘ │ └┐┌┘├┤ ├─┘├┬┘│ ││ ┬├┬┘├┤ └─┐└─┐ + // └─┘└─┘ ┴ ┴ ┴└─┴└─┘┴└─┴ ┴ ┴ └─┘ └┘ └─┘ ┴ ┴└─└─┘└─┘┴└─└─┘└─┘└─┘ + + // Get the JSON array of Priority CVE IDs from the platform record. + let platformRecord = await Platform.find({}).limit(1); + let priorityVulnerabilities = platformRecord[0].priorityCveIds; + let priorityVulnerabilitiesThatExistInTheDatabase = await Vulnerability.find({isPriority: true}); + let cveIdsThatDontExistInTheDatabase = _.difference(priorityVulnerabilities, _.pluck(priorityVulnerabilitiesThatExistInTheDatabase, 'cveId')); + let priorityVulnPatchProgress = []; + + // Get patch progress for priority CVE IDs that only exist in the Platform record. + for(let cve of cveIdsThatDontExistInTheDatabase){ + // Trim whitespace from the CVE ID. https://github.com/fleetdm/fleet/issues/14904 + let trimmedCveId = _.trim(cve); + // Check to see if a Vulnerability record has been created for this CVE ID. + let vulnRecordForThisCveExists = await Vulnerability.findOne({cveId: trimmedCveId, isPriority: false}); + if(vulnRecordForThisCveExists){// If we found a Vulnerability record that matches a CVE ID, we'll update it to have `isPriority: true`. + let updatedVulnRecord = await Vulnerability.updateOne({id: vulnRecordForThisCveExists.id}).set({isPriority: true}); + priorityVulnerabilitiesThatExistInTheDatabase.push(updatedVulnRecord); + } else {// Otherwise, we'll add 100% patch progress for this CVE. + let patchProgress = { + cveId: trimmedCveId, + patchProgressPercentage: 100, + additionalDetailsUrl: 'https://nvd.nist.gov/vuln/detail/'+ encodeURIComponent(cve), + }; + priorityVulnPatchProgress.push(patchProgress); + } + } + + // Get patch progress for CVEs we have records for. + for(let vuln of priorityVulnerabilitiesThatExistInTheDatabase) { + let vulnPatchProgress = _.clone(vuln); + vulnPatchProgress.affectedSoftware = []; + // Calculate how many host have been affected by this vulnerability, and how many hosts are currently affected by this vulnerability + let installsForThisVulnerability = await VulnerabilityInstall.find({vulnerability: vuln.id}); + // This number will represent the number of hosts that have been affected by the vulnerability. + let uniqueAffectedHosts = _.uniq(_.pluck(installsForThisVulnerability, 'host')); + // Get a list of software that is currently installed and affected by this vulnerability. + let unresolvedInstallsForThisVuln = _.filter(installsForThisVulnerability, {uninstalledAt: 0}); + let unresolvedHosts = _.uniq(_.pluck(unresolvedInstallsForThisVuln, 'host')); + let resolvedHosts = _.difference(uniqueAffectedHosts, unresolvedHosts); + let uniqNumberOfResolvedInstallsForThisVuln = resolvedHosts.length; + // Iterate through the installs for this vulnerability to build a list of software + await sails.helpers.flow.simultaneouslyForEach(_.uniq(installsForThisVulnerability, 'fleetApid'), (install)=>{ + vulnPatchProgress.affectedSoftware.push({name: install.softwareName, version: install.versionName, url: sails.config.custom.fleetBaseUrl+'/software/'+install.fleetApid }); + }); + // Get the number of unique hosts who were previosuly affected by this vulnerability. + vulnPatchProgress.numberOfHostsAffected = uniqueAffectedHosts.length; + // To calculate the patch progress, we'll use the number of unique hosts who were previously affected by this vulnerability as the numerator and the number of unique hosts affected by the vulnerability as the denominator. + vulnPatchProgress.patchProgressPercentage = Math.floor((uniqNumberOfResolvedInstallsForThisVuln / vulnPatchProgress.numberOfHostsAffected) * 100); + priorityVulnPatchProgress.push(vulnPatchProgress); + }//∞ + + // Sort the priority vulnerabilities by CVE ID. + priorityVulnPatchProgress = _.sortBy(priorityVulnPatchProgress, 'cveId'); + + return priorityVulnPatchProgress; + + } + + +}; diff --git a/ee/vulnerability-dashboard/api/controllers/update-priority-vulnerabilities.js b/ee/vulnerability-dashboard/api/controllers/update-priority-vulnerabilities.js index 6e10db81c5c3..92de07fc259c 100644 --- a/ee/vulnerability-dashboard/api/controllers/update-priority-vulnerabilities.js +++ b/ee/vulnerability-dashboard/api/controllers/update-priority-vulnerabilities.js @@ -88,25 +88,17 @@ module.exports = { let installsForThisVulnerability = await VulnerabilityInstall.find({vulnerability: vuln.id}); // This number will represent the number of hosts that are currently affected by the vulnerability let uniqueAffectedHosts = _.uniq(_.pluck(installsForThisVulnerability, 'host')); - - let resolvedInstallsForThisVuln = []; - let affectedSoftwareForThisVulnerability = []; - vulnPatchProgress.numberOfHostsAffected = uniqueAffectedHosts.length; - for(let install of installsForThisVulnerability) { - // If the install has a non-zero uninstalledAt value, then it has been uninstalled. - if(install.uninstalledAt !== 0) { - // If this is a resolved install, we'll check the other installs for this vulnerability to see if there is an unresolved VulnerabilityInstall for this host. - if(!_.find(installsForThisVulnerability, {host: install.host, uninstalledAt: 0})){ - // If an unresolved vulnerabilityInstall record affecting this host is found, we won't count this vulnerability as resolved for this host. - resolvedInstallsForThisVuln.push(install); - } - } - affectedSoftwareForThisVulnerability.push({name: install.softwareName, version: install.versionName, url: sails.config.custom.fleetBaseUrl+'/software/'+install.fleetApid }); - }//∞ - + // Get a list of software that is currently installed and affected by this vulnerability. + let unresolvedInstallsForThisVuln = _.filter(installsForThisVulnerability, {uninstalledAt: 0}); + let unresolvedHosts = _.uniq(_.pluck(unresolvedInstallsForThisVuln, 'host')); + let resolvedHosts = _.difference(uniqueAffectedHosts, unresolvedHosts); + let uniqNumberOfResolvedInstallsForThisVuln = resolvedHosts.length; + // Iterate through the installs for this vulnerability to build a list of software + await sails.helpers.flow.simultaneouslyForEach(_.uniq(installsForThisVulnerability, 'fleetApid'), (install)=>{ + vulnPatchProgress.affectedSoftware.push({name: install.softwareName, version: install.versionName, url: sails.config.custom.fleetBaseUrl+'/software/'+install.fleetApid }); + }); // Get the number of unique hosts who were previosuly affected by this vulnerability. - let uniqNumberOfResolvedInstallsForThisVuln = _.uniq(resolvedInstallsForThisVuln, 'host').length; - vulnPatchProgress.affectedSoftware = _.uniq(affectedSoftwareForThisVulnerability, 'url'); + vulnPatchProgress.numberOfHostsAffected = uniqueAffectedHosts.length; // To calculate the patch progress, we'll use the number of unique hosts who were previously affected by this vulnerability as the numerator and the number of unique hosts affected by the vulnerability as the denominator. vulnPatchProgress.patchProgressPercentage = Math.floor((uniqNumberOfResolvedInstallsForThisVuln / vulnPatchProgress.numberOfHostsAffected) * 100); priorityVulnPatchProgress.push(vulnPatchProgress); diff --git a/ee/vulnerability-dashboard/assets/js/cloud.setup.js b/ee/vulnerability-dashboard/assets/js/cloud.setup.js index e230bec29606..b4d0951c46b8 100644 --- a/ee/vulnerability-dashboard/assets/js/cloud.setup.js +++ b/ee/vulnerability-dashboard/assets/js/cloud.setup.js @@ -13,7 +13,7 @@ Cloud.setup({ /* eslint-disable */ - methods: {"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"updatePassword":{"verb":"PUT","url":"/api/v1/account/update-password","args":["password"]},"updateProfile":{"verb":"PUT","url":"/api/v1/account/update-profile","args":["fullName","emailAddress"]},"login":{"verb":"PUT","url":"/api/v1/entrance/login","args":["emailAddress","password","rememberMe"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"signupOktaUserOrRedirect":{"verb":"GET","url":"/entrance/signup-okta-user-or-redirect","args":[]},"getVulnerabilities":{"verb":"GET","url":"/api/v1/get-vulnerabilities","args":["minSeverity","maxSeverity","sortBy","sortDirection","page","teamApid"],"protocol":"io.socket"},"getRemediationTimeline":{"verb":"GET","url":"/api/v1/get-remediation-timeline","args":["vulnerabilityId","teamApid"],"protocol":"io.socket"},"downloadVulnerabilitiesCsv":{"verb":"GET","url":"/download-vulnerabilities-csv","args":["minSeverity","maxSeverity","sortBy","sortDirection","page","teamApid","pageSize","exportType"]},"downloadOneVulnerabilityCsv":{"verb":"GET","url":"/download-one-vulnerability-csv","args":["cveId","teamApid"]},"setCompliantVersions":{"verb":"POST","url":"/api/v1/set-compliant-versions","args":["complianceType","compliantVersions"]},"updatePriorityVulnerabilities":{"verb":"GET","url":"/api/v1/update-priority-vulnerabilities","args":["newPriorityCveIds"]},"getPatchProgressForASingleTeam":{"verb":"GET","url":"/api/v1/get-patch-progress-for-a-single-team","args":["teamApid"]},"downloadUnpatchedHostsCsv":{"verb":"GET","url":"/download-unpatched-hosts-csv","args":["exportType","teamApid"]}} + methods: {"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"updatePassword":{"verb":"PUT","url":"/api/v1/account/update-password","args":["password"]},"updateProfile":{"verb":"PUT","url":"/api/v1/account/update-profile","args":["fullName","emailAddress"]},"login":{"verb":"PUT","url":"/api/v1/entrance/login","args":["emailAddress","password","rememberMe"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"signupOktaUserOrRedirect":{"verb":"GET","url":"/entrance/signup-okta-user-or-redirect","args":[]},"getPriorityVulnerabilities":{"verb":"GET","url":"/api/v1/get-priority-vulnerabilities","args":[]},"getVulnerabilities":{"verb":"GET","url":"/api/v1/get-vulnerabilities","args":["minSeverity","maxSeverity","sortBy","sortDirection","page","teamApid"],"protocol":"io.socket"},"getRemediationTimeline":{"verb":"GET","url":"/api/v1/get-remediation-timeline","args":["vulnerabilityId","teamApid"],"protocol":"io.socket"},"downloadVulnerabilitiesCsv":{"verb":"GET","url":"/download-vulnerabilities-csv","args":["minSeverity","maxSeverity","sortBy","sortDirection","page","teamApid","pageSize","exportType"]},"downloadOneVulnerabilityCsv":{"verb":"GET","url":"/download-one-vulnerability-csv","args":["cveId","teamApid"]},"setCompliantVersions":{"verb":"POST","url":"/api/v1/set-compliant-versions","args":["complianceType","compliantVersions"]},"updatePriorityVulnerabilities":{"verb":"GET","url":"/api/v1/update-priority-vulnerabilities","args":["newPriorityCveIds"]},"getPatchProgressForASingleTeam":{"verb":"GET","url":"/api/v1/get-patch-progress-for-a-single-team","args":["teamApid"]},"downloadUnpatchedHostsCsv":{"verb":"GET","url":"/download-unpatched-hosts-csv","args":["exportType","teamApid"]}} /* eslint-enable */ }); diff --git a/ee/vulnerability-dashboard/assets/js/pages/dashboard/welcome.page.js b/ee/vulnerability-dashboard/assets/js/pages/dashboard/welcome.page.js index d5b62c3dafff..5f991bd440be 100644 --- a/ee/vulnerability-dashboard/assets/js/pages/dashboard/welcome.page.js +++ b/ee/vulnerability-dashboard/assets/js/pages/dashboard/welcome.page.js @@ -48,6 +48,7 @@ parasails.registerPage('welcome', { }] }; await this.drawGraphsOnPage(); + await this.getPriorityCves(); }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ @@ -56,7 +57,6 @@ parasails.registerPage('welcome', { methods: { clickOpenEditModal: function () { this.formData.priorityCves = _.pluck(this.dataForGraphs.priorityVulnPatchProgress, 'cveId'); - console.log(this.formData); this.modal = 'priority-cves'; }, handleSubmittingPriorityCveForm: async function(argins) { @@ -79,6 +79,12 @@ parasails.registerPage('welcome', { this.syncing = false; this.modal = ''; }, + getPriorityCves: async function () { + this.syncing = true; + let priorityCveProgress = await Cloud.getPriorityVulnerabilities(); + this.dataForGraphs.priorityVulnPatchProgress = priorityCveProgress; + this.syncing = false; + }, drawGraphsOnPage: async function(){ new Chart('average-remediation-time', { type: 'line', diff --git a/ee/vulnerability-dashboard/assets/styles/pages/dashboard/welcome.less b/ee/vulnerability-dashboard/assets/styles/pages/dashboard/welcome.less index dcd7ced24e99..4611ac980d15 100644 --- a/ee/vulnerability-dashboard/assets/styles/pages/dashboard/welcome.less +++ b/ee/vulnerability-dashboard/assets/styles/pages/dashboard/welcome.less @@ -100,6 +100,9 @@ text-align: center; } } + [purpose='loading-indicator'] { + .loader(@brand); + } [purpose='vuln-patch-progress-bar'] { width: 300px; height: 16px; diff --git a/ee/vulnerability-dashboard/config/routes.js b/ee/vulnerability-dashboard/config/routes.js index 1960f3352dd1..cf026e4db32c 100644 --- a/ee/vulnerability-dashboard/config/routes.js +++ b/ee/vulnerability-dashboard/config/routes.js @@ -67,7 +67,7 @@ module.exports.routes = { 'GET /entrance/signup-okta-user-or-redirect': { action: 'entrance/signup-okta-user-or-redirect' }, // 'POST /api/v1/deliver-contact-form-message': { action: 'deliver-contact-form-message' }, // 'POST /api/v1/observe-my-session': { action: 'observe-my-session', hasSocketFeatures: true }, - + 'GET /api/v1/get-priority-vulnerabilities': { action: 'get-priority-vulnerabilities' }, 'GET /api/v1/get-vulnerabilities': { action: 'get-vulnerabilities', hasSocketFeatures: true }, 'GET /api/v1/get-remediation-timeline': { action: 'get-remediation-timeline', hasSocketFeatures: true }, 'GET /download-vulnerabilities-csv': { action: 'download-vulnerabilities-csv'}, diff --git a/ee/vulnerability-dashboard/views/pages/dashboard/welcome.ejs b/ee/vulnerability-dashboard/views/pages/dashboard/welcome.ejs index bdc36573baa0..f4fe23c906a1 100644 --- a/ee/vulnerability-dashboard/views/pages/dashboard/welcome.ejs +++ b/ee/vulnerability-dashboard/views/pages/dashboard/welcome.ejs @@ -6,14 +6,25 @@

Priority CVEs

- + a small pencilEdit -
+

No priority CVEs

Configure a list here to track your organization's progress patching particular vulnerabilities.

+
+

Loading priority CVE progress

+

+ + + + + + +

+

This is the list of particular CVEs your organization is currently working to patch.

From 65341be05499898f631bed0615b44e3a5cfefbed Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 15 Aug 2024 12:58:22 -0500 Subject: [PATCH 2/2] Update get-priority-vulnerabilities.js --- .../api/controllers/get-priority-vulnerabilities.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ee/vulnerability-dashboard/api/controllers/get-priority-vulnerabilities.js b/ee/vulnerability-dashboard/api/controllers/get-priority-vulnerabilities.js index 96a1ff8ba95e..9f55ec19c8d7 100644 --- a/ee/vulnerability-dashboard/api/controllers/get-priority-vulnerabilities.js +++ b/ee/vulnerability-dashboard/api/controllers/get-priority-vulnerabilities.js @@ -7,18 +7,14 @@ module.exports = { description: 'Returns information about priority CVEs.', extendedDescription: 'This code was previously in the view action for the dashboard page, but was moved to a separate action to reduce inital page laoding time.', - - inputs: { - - }, - - exits: { - + success: { + outputType: [{}], + }, }, - fn: async function (inputs) { + fn: async function () { // ┌─┐┌─┐┌┬┐ ┌─┐┬─┐┬┌─┐┬─┐┬┌┬┐┬ ┬ ┌─┐┬ ┬┌─┐ ┌─┐┬─┐┌─┐┌─┐┬─┐┌─┐┌─┐┌─┐ // │ ┬├┤ │ ├─┘├┬┘││ │├┬┘│ │ └┬┘ │ └┐┌┘├┤ ├─┘├┬┘│ ││ ┬├┬┘├┤ └─┐└─┐ // └─┘└─┘ ┴ ┴ ┴└─┴└─┘┴└─┴ ┴ ┴ └─┘ └┘ └─┘ ┴ ┴└─└─┘└─┘┴└─└─┘└─┘└─┘