diff --git a/ee/vulnerability-dashboard/api/controllers/view-dashboard.js b/ee/vulnerability-dashboard/api/controllers/view-dashboard.js
new file mode 100644
index 0000000000..5d38fe7baf
--- /dev/null
+++ b/ee/vulnerability-dashboard/api/controllers/view-dashboard.js
@@ -0,0 +1,1476 @@
+module.exports = {
+
+
+ friendlyName: 'View dashboard',
+
+
+ description: 'Display "Dashboard" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/dashboard'
+ }
+
+ },
+
+
+ fn: async function () {
+ // console.time(`Dashboard page view action`);
+ let dataForGraphs = {};
+
+ // Get the most recently created Vulnerability and VulnerabilityInstall record.
+ let latestVulnerabilityInstallRecord = await VulnerabilityInstall.find().sort('updatedAt DESC').limit(1);
+ let latestVulnerabilityRecord = await Vulnerability.find().sort('updatedAt DESC').limit(1);
+ // Get the most recent createdAt timestamp from these records, we'll use this timestamp to set the "Last updated:" on the dashboard page.
+ let latestTimestamp = Math.max(latestVulnerabilityRecord[0].updatedAt, latestVulnerabilityInstallRecord[0].updatedAt);
+ let dashboardDataLastUpdatedAt = latestTimestamp;
+ dataForGraphs.lastUpdatedAt = dashboardDataLastUpdatedAt;
+
+
+ //
+ // ╦ ╦╦ ╦╦ ╔╗╔╔═╗╦═╗╔═╗╔╗ ╦╦ ╦╔╦╗╦ ╦ ╔═╗╔═╗╦ ╦╔╗╔╔╦╗╔═╗
+ // ╚╗╔╝║ ║║ ║║║║╣ ╠╦╝╠═╣╠╩╗║║ ║ ║ ╚╦╝ ║ ║ ║║ ║║║║ ║ ╚═╗
+ // ╚╝ ╚═╝╩═╝╝╚╝╚═╝╩╚═╩ ╩╚═╝╩╩═╝╩ ╩ ╩ ╚═╝╚═╝╚═╝╝╚╝ ╩ ╚═╝
+ // Note: the information gathered in this section is used in the unique vulnerability count tiles, as well as the "What percentage are critical?" chart.
+ let nativeQueryToFindAllVulnerabilityIdsWithSeverity;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ nativeQueryToFindAllVulnerabilityIdsWithSeverity = `
+ SELECT
+ vulnerability.id,
+ vulnerability.severity
+ FROM vulnerability
+ WHERE EXISTS (
+ SELECT 1
+ FROM vulnerabilityinstall
+ WHERE vulnerabilityinstall.vulnerability = vulnerability.id
+ AND (vulnerabilityinstall."uninstalledAt" IS NULL OR vulnerabilityinstall."uninstalledAt" = 0)
+ );`;
+ } else {
+ nativeQueryToFindAllVulnerabilityIdsWithSeverity = `
+ SELECT
+ vulnerability.id,
+ vulnerability.severity
+ FROM vulnerability
+ WHERE EXISTS (
+ SELECT 1
+ FROM vulnerabilityinstall
+ WHERE vulnerabilityinstall.vulnerability = vulnerability.id
+ AND (vulnerabilityinstall.uninstalledAt IS NULL
+ OR vulnerabilityinstall.uninstalledAt = 0)
+ );`;
+ }
+
+ // console.time('all vulns native query');
+ let vulnerabilityNativeQueryResult = await sails.sendNativeQuery(nativeQueryToFindAllVulnerabilityIdsWithSeverity);
+ // console.timeEnd('all vulns native query');
+
+ let vulnerabilityIdsWithSeverity = vulnerabilityNativeQueryResult.rows;
+
+ // Sort the vulnerabilities by severity, and add the counts to the totalUniqueCounts dictionary.
+ let criticalSeverity = vulnerabilityIdsWithSeverity.filter((vuln)=>{
+ return vuln.severity >= 9;
+ });
+ let highSeverity = vulnerabilityIdsWithSeverity.filter((vuln)=>{
+ return vuln.severity <= 8.9 && vuln.severity >= 7.0;
+ });
+ let mediumSeverity = vulnerabilityIdsWithSeverity.filter((vuln)=>{
+ return vuln.severity <= 6.9 && vuln.severity >= 4.0;
+ });
+ let lowSeverity = vulnerabilityIdsWithSeverity.filter((vuln)=>{
+ return vuln.severity >= 0 && vuln.severity <= 3.9;
+ });
+
+ let totalUniqueCounts = {
+ low: lowSeverity.length,
+ medium: mediumSeverity.length,
+ high: highSeverity.length,
+ critical: criticalSeverity.length,
+ };
+ dataForGraphs.totalUniqueCounts = totalUniqueCounts;
+
+ // Now build native SQL Queries using the IDs of vulnerabilities to find vulnerabilityInstall records for each severity.
+ let lowVulnerabilityInstallNativeQuery;
+ let mediumVulnerabilityInstallNativeQuery;
+ let highVulnerabilityInstallNativeQuery;
+ let criticalVulnerabilityInstallNativeQuery;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ // If this app is configured to use a Postgres datastore, we'll need to put double quotes around the uninstalledAt column name.
+ lowVulnerabilityInstallNativeQuery = `
+ SELECT * FROM vulnerabilityinstall
+ WHERE "uninstalledAt" = 0 AND vulnerability IN (${_.pluck(lowSeverity,'id').join(',')});`;
+ mediumVulnerabilityInstallNativeQuery = `
+ SELECT * FROM vulnerabilityinstall
+ WHERE "uninstalledAt" = 0 AND vulnerability IN (${_.pluck(mediumSeverity,'id').join(',')})`;
+ highVulnerabilityInstallNativeQuery = `
+ SELECT * FROM vulnerabilityinstall
+ WHERE "uninstalledAt" = 0 AND vulnerability IN (${_.pluck(highSeverity,'id').join(',')})`;
+ criticalVulnerabilityInstallNativeQuery = `
+ SELECT * FROM vulnerabilityinstall
+ WHERE "uninstalledAt" = 0 AND vulnerability IN (${_.pluck(criticalSeverity,'id').join(',')})`;
+ } else if(sails.config.datastores.default.adapter === 'sails-mysql') {
+ lowVulnerabilityInstallNativeQuery = `
+ SELECT * FROM vulnerabilityinstall
+ WHERE uninstalledAt = 0 AND vulnerability IN (${_.pluck(lowSeverity,'id').join(',')})`;
+ mediumVulnerabilityInstallNativeQuery = `
+ SELECT * FROM vulnerabilityinstall
+ WHERE uninstalledAt = 0 AND vulnerability IN (${_.pluck(mediumSeverity,'id').join(',')})`;
+ highVulnerabilityInstallNativeQuery = `
+ SELECT * FROM vulnerabilityinstall
+ WHERE uninstalledAt = 0 AND vulnerability IN (${_.pluck(highSeverity,'id').join(',')})`;
+ criticalVulnerabilityInstallNativeQuery = `
+ SELECT * FROM vulnerabilityinstall
+ WHERE uninstalledAt = 0 AND vulnerability IN (${_.pluck(criticalSeverity,'id').join(',')})`;
+ }
+
+ // console.time(`native queries to get total counts`);
+ // Now send some queries.
+ let criticalVulnInstallsResult = await sails.sendNativeQuery(criticalVulnerabilityInstallNativeQuery);
+ let totalInstallsWithCricitalVulns = criticalVulnInstallsResult.rows;
+ let highVulnInstallsResult = await sails.sendNativeQuery(highVulnerabilityInstallNativeQuery);
+ let totalInstallsWithHighVulns = highVulnInstallsResult.rows;
+ let mediumVulnInstallsResult = await sails.sendNativeQuery(mediumVulnerabilityInstallNativeQuery);
+ let totalInstallsWithMediumVulns = mediumVulnInstallsResult.rows;
+ let lowVulnInstallsResult = await sails.sendNativeQuery(lowVulnerabilityInstallNativeQuery);
+ let totalInstallsWithLowVulns = lowVulnInstallsResult.rows;
+ // console.timeEnd(`native queries to get total counts`);
+
+ let totalNumberOfVulns = {
+ low: totalInstallsWithLowVulns.length,
+ medium: totalInstallsWithMediumVulns.length,
+ high: totalInstallsWithHighVulns.length,
+ critical: totalInstallsWithCricitalVulns.length
+ };
+ dataForGraphs.totalNumberOfVulns = totalNumberOfVulns;
+
+
+
+ // ╔╦╗╦ ╦╔═╗ ╦═╗╦╔═╗╦╔═╦╔═╗╔═╗╔╦╗ ╦ ╦╔═╗╔═╗╔╦╗╔═╗ ╦═╗╦╔═╗╦ ╦╔╦╗ ╔╗╔╔═╗╦ ╦
+ // ║ ╠═╣║╣ ╠╦╝║╚═╗╠╩╗║║╣ ╚═╗ ║ ╠═╣║ ║╚═╗ ║ ╚═╗ ╠╦╝║║ ╦╠═╣ ║ ║║║║ ║║║║
+ // ╩ ╩ ╩╚═╝ ╩╚═╩╚═╝╩ ╩╩╚═╝╚═╝ ╩ ╩ ╩╚═╝╚═╝ ╩ ╚═╝ ╩╚═╩╚═╝╩ ╩ ╩ ╝╚╝╚═╝╚╩╝
+
+ let nativeQueryToFindTopTenMostVulnerableHosts;
+
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+
+ nativeQueryToFindTopTenMostVulnerableHosts = `
+ WITH "installAggregates" AS (
+ SELECT
+ vulnerabilityinstall.vulnerability AS "vulnerabilityId",
+ COUNT(DISTINCT CASE
+ WHEN vulnerabilityinstall."uninstalledAt" IS NULL OR vulnerabilityinstall."uninstalledAt" = 0
+ THEN vulnerabilityinstall.host END
+ ) AS "currentAffectedHostCount",
+ COUNT(DISTINCT vulnerabilityinstall.host) AS "totalAffectedHostCount"
+ FROM vulnerabilityinstall
+ GROUP BY vulnerabilityinstall.vulnerability
+ ),
+ "riskCalculation" AS (
+ SELECT
+ vulnerability.id AS "vulnerabilityId",
+ "installAggregates"."currentAffectedHostCount",
+ "installAggregates"."totalAffectedHostCount",
+ (
+ vulnerability.severity *
+ CASE
+ WHEN vulnerability."hasKnownExploit" THEN 2.0
+ WHEN vulnerability."probabilityOfExploit" > 0.5 THEN 1.5
+ ELSE 1.0
+ END
+ ) AS "threatScore",
+ LN(1 + COALESCE("installAggregates"."currentAffectedHostCount", 0)) AS "impactScore"
+ FROM vulnerability
+ LEFT JOIN "installAggregates"
+ ON "installAggregates"."vulnerabilityId" = vulnerability.id
+ ),
+ "normalizedRisk" AS (
+ SELECT
+ "vulnerabilityId",
+ ("threatScore" * "impactScore") AS "rawRiskScore",
+ MAX("threatScore" * "impactScore") OVER () AS "maxRawRisk"
+ FROM "riskCalculation"
+ ),
+ "vulnerabilityPps" AS (
+ SELECT
+ "vulnerabilityId",
+ CASE
+ WHEN "maxRawRisk" > 0
+ THEN ("rawRiskScore" / "maxRawRisk" * 9.9) + 0.1
+ ELSE 0.1
+ END AS "vulnerabilityPps"
+ FROM "normalizedRisk"
+ ),
+ "hostVulnerabilitySet" AS (
+ SELECT DISTINCT
+ vulnerabilityinstall.host AS "hostId",
+ vulnerability.id AS "vulnerabilityId",
+ vulnerability."hasKnownExploit" AS "hasKnownExploit",
+ "vulnerabilityPps"."vulnerabilityPps" AS "vulnerabilityPps"
+ FROM vulnerabilityinstall
+ JOIN vulnerability
+ ON vulnerability.id = vulnerabilityinstall.vulnerability
+ JOIN "vulnerabilityPps"
+ ON "vulnerabilityPps"."vulnerabilityId" = vulnerability.id
+ WHERE vulnerabilityinstall."uninstalledAt" IS NULL
+ OR vulnerabilityinstall."uninstalledAt" = 0
+ ),
+ "hostPps" AS (
+ SELECT
+ host."displayName",
+ host."fleetApid",
+ COUNT(DISTINCT "hostVulnerabilitySet"."vulnerabilityId") AS "vulnerabilityCount",
+ COUNT(DISTINCT "hostVulnerabilitySet"."vulnerabilityId") FILTER (WHERE "hostVulnerabilitySet"."hasKnownExploit") AS "kevCount",
+ MAX("hostVulnerabilitySet"."vulnerabilityPps") AS "maxVulnerabilityPps",
+ (
+ MAX("hostVulnerabilitySet"."vulnerabilityPps")
+ * (1 + LN(1 + COUNT(DISTINCT "hostVulnerabilitySet"."vulnerabilityId")) * 0.15)
+ )::numeric(12,4) AS "hostPpsNumeric"
+ FROM "hostVulnerabilitySet"
+ JOIN host ON host.id = "hostVulnerabilitySet"."hostId"
+ GROUP BY host."displayName", host."fleetApid"
+ ),
+ "hostPpsNormalized" AS (
+ SELECT
+ "hostPps"."displayName",
+ "hostPps"."fleetApid",
+ "hostPps"."vulnerabilityCount",
+ "hostPps"."kevCount",
+ "hostPps"."maxVulnerabilityPps",
+ "hostPps"."hostPpsNumeric",
+ MAX("hostPps"."hostPpsNumeric") OVER () AS "maxHostPps"
+ FROM "hostPps"
+ )
+ SELECT
+ "hostPpsNormalized"."displayName",
+ "hostPpsNormalized"."fleetApid",
+ "hostPpsNormalized"."vulnerabilityCount",
+ "hostPpsNormalized"."kevCount",
+ ROUND("hostPpsNormalized"."maxVulnerabilityPps"::numeric, 2) AS "worstVulnerabilityPps",
+ CASE
+ WHEN "hostPpsNormalized"."maxHostPps" > 0
+ THEN ROUND( ( "hostPpsNormalized"."hostPpsNumeric" / "hostPpsNormalized"."maxHostPps" ) * 9 + 1, 2)
+ ELSE 1.00
+ END AS "hostPps"
+ FROM "hostPpsNormalized"
+ ORDER BY "hostPps" DESC, "hostPpsNormalized"."displayName" ASC
+ LIMIT 10;`;
+ } else {
+ nativeQueryToFindTopTenMostVulnerableHosts = `
+ WITH installAggregates AS (
+ SELECT
+ vi.vulnerability AS vulnerabilityId,
+ COUNT(DISTINCT IF(vi.uninstalledAt IS NULL OR vi.uninstalledAt = 0, vi.host, NULL)) AS currentAffectedHostCount,
+ COUNT(DISTINCT vi.host) AS totalAffectedHostCount
+ FROM vulnerabilityinstall vi
+ GROUP BY vi.vulnerability
+ ),
+ riskCalculation AS (
+ SELECT
+ v.id AS vulnerabilityId,
+ ia.currentAffectedHostCount,
+ ia.totalAffectedHostCount,
+ (
+ v.severity *
+ CASE
+ WHEN v.hasKnownExploit THEN 2.0
+ WHEN v.probabilityOfExploit > 0.5 THEN 1.5
+ ELSE 1.0
+ END
+ ) AS threatScore,
+ LN(1 + COALESCE(ia.currentAffectedHostCount, 0)) AS impactScore
+ FROM vulnerability v
+ LEFT JOIN installAggregates ia ON ia.vulnerabilityId = v.id
+ ),
+ normalizedRisk AS (
+ SELECT
+ vulnerabilityId,
+ (threatScore * impactScore) AS rawRiskScore,
+ MAX(threatScore * impactScore) OVER () AS maxRawRisk
+ FROM riskCalculation
+ ),
+ vulnerabilityPps AS (
+ SELECT
+ vulnerabilityId,
+ CASE
+ WHEN maxRawRisk > 0
+ THEN (rawRiskScore / maxRawRisk * 9.9) + 0.1
+ ELSE 0.1
+ END AS vulnerabilityPps
+ FROM normalizedRisk
+ ),
+ hostVulnerabilitySet AS (
+ SELECT DISTINCT
+ vi.host AS hostId,
+ v.id AS vulnerabilityId,
+ v.hasKnownExploit AS hasKnownExploit,
+ vp.vulnerabilityPps AS vulnerabilityPps
+ FROM vulnerabilityinstall vi
+ JOIN vulnerability v ON v.id = vi.vulnerability
+ JOIN vulnerabilityPps vp ON vp.vulnerabilityId = v.id
+ WHERE vi.uninstalledAt IS NULL OR vi.uninstalledAt = 0
+ ),
+ hostPps AS (
+ SELECT
+ h.displayName,
+ h.fleetApid,
+ COUNT(DISTINCT hvs.vulnerabilityId) AS vulnerabilityCount,
+ COUNT(DISTINCT IF(hvs.hasKnownExploit, hvs.vulnerabilityId, NULL)) AS kevCount,
+ MAX(hvs.vulnerabilityPps) AS maxVulnerabilityPps,
+ CAST(
+ MAX(hvs.vulnerabilityPps) * (1 + LN(1 + COUNT(DISTINCT hvs.vulnerabilityId)) * 0.15)
+ AS DECIMAL(12,4)
+ ) AS hostPpsNumeric
+ FROM hostVulnerabilitySet hvs
+ JOIN host h ON h.id = hvs.hostId
+ GROUP BY h.displayName, h.fleetApid
+ ),
+ hostPpsNormalized AS (
+ SELECT
+ hp.displayName,
+ hp.fleetApid,
+ hp.vulnerabilityCount,
+ hp.kevCount,
+ hp.maxVulnerabilityPps,
+ hp.hostPpsNumeric,
+ MAX(hp.hostPpsNumeric) OVER () AS maxHostPps
+ FROM hostPps hp
+ )
+ SELECT
+ displayName,
+ fleetApid,
+ vulnerabilityCount,
+ kevCount,
+ ROUND(maxVulnerabilityPps, 2) AS worstVulnerabilityPps,
+ CASE
+ WHEN maxHostPps > 0
+ THEN ROUND(((hostPpsNumeric / maxHostPps) * 9) + 1, 2)
+ ELSE 1.00
+ END AS hostPps
+ FROM hostPpsNormalized
+ ORDER BY hostPps DESC, displayName ASC
+ LIMIT 10;`;
+ }
+
+ // console.time(`native query to find top 10 most vulnerable hosts`);
+ let topTenMostVulnerableHosts = await sails.sendNativeQuery(nativeQueryToFindTopTenMostVulnerableHosts);
+ // console.timeEnd(`native query to find top 10 most vulnerable hosts`);
+
+ // console.log(topTenMostVulnerableHosts.rows);
+ let mostVulnerableHosts = topTenMostVulnerableHosts.rows.map((host)=>{
+ return {
+ displayName: host.displayName,
+ fleetUrl: sails.config.custom.fleetBaseUrl + `/hosts/${host.fleetApid}`,
+ numberOfCves: host.vulnerabilityCount,
+ pps: host.hostPps,
+ };
+ });
+ dataForGraphs.mostVulnerableHosts = mostVulnerableHosts;
+
+
+
+ // ╔╦╗╦ ╦╔═╗ ╦ ╦╔═╗╦ ╦╔═╗╦ ╔═╗╦ ╦╔═╗╔═╗╔═╗╔═╗╔╦╗╔═╗ ╦╔╗╔ ╔═╗╔═╗╔═╗╔╦╗╦ ╦╔═╗╦═╗╔═╗
+ // ║ ╠═╣║╣ ║ ║╚═╗║ ║╠═╣║ ╚═╗║ ║╚═╗╠═╝║╣ ║ ║ ╚═╗ ║║║║ ╚═╗║ ║╠╣ ║ ║║║╠═╣╠╦╝║╣
+ // ╩ ╩ ╩╚═╝ ╚═╝╚═╝╚═╝╩ ╩╩═╝ ╚═╝╚═╝╚═╝╩ ╚═╝╚═╝ ╩ ╚═╝ ╩╝╚╝ ╚═╝╚═╝╚ ╩ ╚╩╝╩ ╩╩╚═╚═╝
+ let nativeQueryToFindTopTenMostVulnerableSoftwareItems;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ nativeQueryToFindTopTenMostVulnerableSoftwareItems = `
+ SELECT
+ vi."softwareName",
+ vi."versionName",
+ MIN(vi."fleetApid") AS "fleetApid",
+ COUNT(DISTINCT v.id) AS "numberOfVulns"
+ FROM vulnerabilityinstall vi
+ JOIN vulnerability v
+ ON vi.vulnerability = v.id
+ WHERE COALESCE(vi."uninstalledAt", 0) = 0
+ GROUP BY
+ vi."softwareName",
+ vi."versionName"
+ ORDER BY
+ "numberOfVulns" DESC
+ LIMIT 10;`;
+ } else {
+ nativeQueryToFindTopTenMostVulnerableSoftwareItems = `
+ SELECT
+ vi.softwareName,
+ vi.versionName,
+ MIN(vi.fleetApid) AS fleetApid,
+ COUNT(DISTINCT v.id) AS numberOfVulns
+ FROM vulnerabilityinstall vi
+ JOIN vulnerability v
+ ON vi.vulnerability = v.id
+ WHERE vi.uninstalledAt IS NULL OR vi.uninstalledAt = 0
+ GROUP BY
+ vi.softwareName,
+ vi.versionName
+ ORDER BY
+ numberOfVulns DESC
+ LIMIT 10;`;
+ }
+
+ // console.time(`top ten most vulenerable software query`);
+ let topVulnerableSoftwareResult = await sails.sendNativeQuery(nativeQueryToFindTopTenMostVulnerableSoftwareItems);
+ // console.timeEnd(`top ten most vulenerable software query`);
+
+ let topTenMostVulnerableSoftwareItems = topVulnerableSoftwareResult.rows.map((software)=>{
+ return {
+ softwareNameAndVersion: `${software.softwareName} - ${software.versionName}`,
+ fleetUrl: sails.config.custom.fleetBaseUrl + `/software/versions/${software.fleetApid}`,
+ numberOfVulns: software.numberOfVulns,
+ };
+ });
+
+ dataForGraphs.mostVulnerableSoftware = topTenMostVulnerableSoftwareItems;
+
+
+ // ╔═╗╦═╗╦╔╦╗╦╔═╗╔═╗╦ ╦ ╦ ╦ ╦═╗╔═╗╦═╗╔═╗ ╔═╗╔═╗╔═╗╔╦╗╦ ╦╔═╗╦═╗╔═╗
+ // ║ ╠╦╝║ ║ ║║ ╠═╣║ ║ ╚╦╝ ╠╦╝╠═╣╠╦╝║╣ ╚═╗║ ║╠╣ ║ ║║║╠═╣╠╦╝║╣
+ // ╚═╝╩╚═╩ ╩ ╩╚═╝╩ ╩╩═╝╩═╝╩ ╩╚═╩ ╩╩╚═╚═╝ ╚═╝╚═╝╚ ╩ ╚╩╝╩ ╩╩╚═╚═╝
+ // Software that is installed on <5 hosts with critical vulnerabilities.
+ let queryToFindTopUniqueCriticalSoftwareWithMostVulnerabilities;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+
+ queryToFindTopUniqueCriticalSoftwareWithMostVulnerabilities = `
+ SELECT
+ vi."softwareName",
+ STRING_AGG(DISTINCT h."displayName", ', ' ORDER BY h."displayName") AS "hostNames",
+ STRING_AGG(DISTINCT v."cveId", ', ' ORDER BY v."cveId") AS cves,
+ STRING_AGG(DISTINCT v."cveDescription", '; ' ORDER BY v."cveDescription") AS descriptions,
+ COUNT(DISTINCT vi.host) AS host_install_count
+ FROM vulnerabilityinstall vi
+ JOIN vulnerability v ON vi.vulnerability = v.id
+ JOIN host h ON vi.host = h.id
+ WHERE
+ (vi."uninstalledAt" IS NULL OR vi."uninstalledAt" = 0)
+ AND v.severity >= 9.0
+ AND vi."softwareName" !~* '^(Python|perl|lib)'
+ GROUP BY
+ vi."softwareName"
+ HAVING
+ COUNT(DISTINCT vi.host) < 3
+ ORDER BY
+ host_install_count ASC
+ LIMIT 10;`;
+ // queryToFindTopUniqueCriticalSoftwareWithMostVulnerabilities = `
+ // WITH "activeInstalls" AS (
+ // SELECT
+ // vulnerabilityinstall.host,
+ // vulnerabilityinstall."softwareName",
+ // vulnerabilityinstall."versionName",
+ // vulnerabilityinstall."fleetApid",
+ // vulnerabilityinstall."softwareName" AS "softwareNameAndVersion"
+ // FROM vulnerabilityinstall
+ // WHERE vulnerabilityinstall."uninstalledAt" IS NULL OR vulnerabilityinstall."uninstalledAt" = 0
+ // ),
+ // "softwareHostCounts" AS (
+ // SELECT
+ // "activeInstalls"."softwareNameAndVersion",
+ // COUNT(DISTINCT "activeInstalls".host) AS "hostInstallCount",
+ // STRING_AGG(DISTINCT host."displayName", ', ' ORDER BY host."displayName") AS "hostNames",
+ // STRING_AGG(
+ // DISTINCT "activeInstalls"."fleetApid"::text,
+ // ', ' ORDER BY "activeInstalls"."fleetApid"::text
+ // ) AS "fleetApid"
+ // FROM "activeInstalls"
+ // JOIN host ON host.id = "activeInstalls".host
+ // GROUP BY "activeInstalls"."softwareNameAndVersion"
+ // ),
+ // "criticalVulnerabilityAgg" AS (
+ // SELECT
+ // "activeInstalls"."softwareNameAndVersion",
+ // COUNT(DISTINCT vulnerability.id) AS "vulnerabilityCount",
+ // STRING_AGG(DISTINCT vulnerability."cveId", ', ' ORDER BY vulnerability."cveId") AS "cveIds"
+ // FROM "activeInstalls"
+ // JOIN vulnerabilityinstall ON
+ // vulnerabilityinstall.host = "activeInstalls".host
+ // AND vulnerabilityinstall."softwareName" = "activeInstalls"."softwareName"
+ // AND COALESCE(vulnerabilityinstall."versionName",'') = COALESCE("activeInstalls"."versionName",'')
+ // AND (vulnerabilityinstall."uninstalledAt" IS NULL OR vulnerabilityinstall."uninstalledAt" = 0)
+ // JOIN vulnerability ON vulnerability.id = vulnerabilityinstall.vulnerability
+ // WHERE vulnerability.severity >= 9.0
+ // GROUP BY "activeInstalls"."softwareNameAndVersion"
+ // )
+ // SELECT
+ // "softwareHostCounts"."softwareNameAndVersion",
+ // "softwareHostCounts"."hostInstallCount",
+ // "criticalVulnerabilityAgg"."vulnerabilityCount",
+ // "criticalVulnerabilityAgg"."cveIds",
+ // "softwareHostCounts"."hostNames",
+ // "softwareHostCounts"."fleetApid"
+ // FROM "softwareHostCounts"
+ // JOIN "criticalVulnerabilityAgg"
+ // ON "criticalVulnerabilityAgg"."softwareNameAndVersion" = "softwareHostCounts"."softwareNameAndVersion"
+ // WHERE "softwareHostCounts"."hostInstallCount" < 5
+ // ORDER BY
+ // "criticalVulnerabilityAgg"."vulnerabilityCount" DESC,
+ // "softwareHostCounts"."hostInstallCount" ASC,
+ // "softwareHostCounts"."softwareNameAndVersion" ASC
+ // LIMIT 10;`;
+ } else {
+ queryToFindTopUniqueCriticalSoftwareWithMostVulnerabilities = `
+ SELECT
+ vi.softwareName,
+ GROUP_CONCAT(DISTINCT h.displayName SEPARATOR ', ') AS host_names,
+ GROUP_CONCAT(DISTINCT v.cveId SEPARATOR ', ') AS cves,
+ GROUP_CONCAT(DISTINCT v.cveDescription SEPARATOR '; ') AS descriptions,
+ COUNT(DISTINCT vi.host) AS host_install_count
+ FROM
+ vulnerabilityinstall vi
+ JOIN
+ vulnerability v ON vi.vulnerability = v.id
+ JOIN
+ host h ON vi.host = h.id
+ WHERE
+ -- Consider only currently installed software
+ (vi.uninstalledAt IS NULL OR vi.uninstalledAt = 0)
+ -- Filter for software associated with 'Critical' vulnerabilities (CVSS 8.0-10.0)
+ AND v.severity >= 8.0
+ -- Exclude software names starting with 'Python' or 'pert'
+ AND vi.softwareName NOT REGEXP '^(Python|perl|lib)'
+ GROUP BY
+ vi.softwareName
+ HAVING
+ host_install_count < 3
+ ORDER BY
+ -- Order by the number of hosts the software is installed on, ascending,
+ -- to show the rarest software first.
+ host_install_count ASC
+ LIMIT 10`;
+ // queryToFindTopUniqueCriticalSoftwareWithMostVulnerabilities = `
+ // WITH activeInstalls AS (
+ // SELECT
+ // vulnerabilityinstall.host,
+ // vulnerabilityinstall.softwareName,
+ // vulnerabilityinstall.versionName,
+ // vulnerabilityinstall.fleetApid,
+ // CONCAT(vulnerabilityinstall.softwareName AS softwareNameAndVersion
+ // FROM vulnerabilityinstall
+ // WHERE vulnerabilityinstall.uninstalledAt IS NULL OR vulnerabilityinstall.uninstalledAt = 0
+ // ),
+ // softwareHostCounts AS (
+ // SELECT
+ // activeInstalls.softwareNameAndVersion,
+ // COUNT(DISTINCT activeInstalls.host) AS hostInstallCount,
+ // GROUP_CONCAT(DISTINCT host.displayName ORDER BY host.displayName SEPARATOR ', ') AS hostNames,
+ // GROUP_CONCAT(DISTINCT CAST(activeInstalls.fleetApid AS CHAR) ORDER BY CAST(activeInstalls.fleetApid AS CHAR) SEPARATOR ', ') AS fleetApid
+ // FROM activeInstalls
+ // JOIN host ON host.id = activeInstalls.host
+ // GROUP BY activeInstalls.softwareNameAndVersion
+ // ),
+ // criticalVulnerabilityAgg AS (
+ // SELECT
+ // activeInstalls.softwareNameAndVersion,
+ // COUNT(DISTINCT vulnerability.id) AS vulnerabilityCount,
+ // GROUP_CONCAT(DISTINCT vulnerability.cveId ORDER BY vulnerability.cveId SEPARATOR ', ') AS cveIds
+ // FROM activeInstalls
+ // JOIN vulnerabilityinstall ON
+ // vulnerabilityinstall.host = activeInstalls.host
+ // AND vulnerabilityinstall.softwareName = activeInstalls.softwareName
+ // AND COALESCE(vulnerabilityinstall.versionName,'') = COALESCE(activeInstalls.versionName,'')
+ // AND (vulnerabilityinstall.uninstalledAt IS NULL OR vulnerabilityinstall.uninstalledAt = 0)
+ // JOIN vulnerability ON vulnerability.id = vulnerabilityinstall.vulnerability
+ // WHERE vulnerability.severity >= 9.0
+ // GROUP BY activeInstalls.softwareNameAndVersion
+ // )
+ // SELECT
+ // softwareHostCounts.softwareNameAndVersion,
+ // softwareHostCounts.hostInstallCount,
+ // criticalVulnerabilityAgg.vulnerabilityCount,
+ // criticalVulnerabilityAgg.cveIds,
+ // softwareHostCounts.hostNames,
+ // softwareHostCounts.fleetApid
+ // FROM softwareHostCounts
+ // JOIN criticalVulnerabilityAgg
+ // ON criticalVulnerabilityAgg.softwareNameAndVersion = softwareHostCounts.softwareNameAndVersion
+ // WHERE softwareHostCounts.hostInstallCount < 5
+ // ORDER BY
+ // criticalVulnerabilityAgg.vulnerabilityCount DESC,
+ // softwareHostCounts.hostInstallCount ASC,
+ // softwareHostCounts.softwareNameAndVersion ASC
+ // LIMIT 10;`;
+ }
+ // console.time('critical unique software query');
+ let topUniqueCriticalVulnsResult = await sails.sendNativeQuery(queryToFindTopUniqueCriticalSoftwareWithMostVulnerabilities);
+ // console.timeEnd('critical unique software query');
+ // console.log(topUniqueCriticalVulnsResult);
+ let uniqueCriticalSoftware = topUniqueCriticalVulnsResult.rows.map((software)=>{
+ // Build an array of CVE objects, with each containing the CVE ID and a link to the Fleet page for the CVE.
+ let cves = software.cves.split(', ').map((cveId)=>{
+ return {
+ name: cveId,
+ fleetUrl: `${sails.config.custom.fleetBaseUrl}/software/vulnerabilities/${cveId}`
+ };
+ });
+
+ return {
+ softwareNameAndVersion: software.softwareName,
+ fleetUrl: `${sails.config.custom.fleetBaseUrl}/software/versions/${software.fleetApid}`,
+ numberOfVulns: cves.length,
+ cves,
+ affectedHosts: software.hostNames.split(', '),
+ };
+ });
+ dataForGraphs.uniqueCriticalSoftware = uniqueCriticalSoftware;
+ // console.log(uniqueCriticalSoftware);
+
+
+
+ //
+ // ╦ ╦╔═╗╔═╗╔╦╗╔═╗ ╦ ╦╦╔╦╗╦ ╦ ╔╦╗╔═╗╔═╗╔╦╗ ╔═╗╦ ╦╔═╗╔═╗
+ // ╠═╣║ ║╚═╗ ║ ╚═╗ ║║║║ ║ ╠═╣ ║║║║ ║╚═╗ ║ ║ ╚╗╔╝║╣ ╚═╗
+ // ╩ ╩╚═╝╚═╝ ╩ ╚═╝ ╚╩╝╩ ╩ ╩ ╩ ╩ ╩╚═╝╚═╝ ╩ ╚═╝ ╚╝ ╚═╝╚═╝
+ let hostsWithMostVulnsQuery;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ hostsWithMostVulnsQuery = `
+ WITH "activeVulnerabilityInstalls" AS (
+ SELECT DISTINCT
+ vulnerabilityinstall.host,
+ vulnerabilityinstall.vulnerability
+ FROM vulnerabilityinstall
+ WHERE
+ vulnerabilityinstall."uninstalledAt" IS NULL
+ OR vulnerabilityinstall."uninstalledAt" = 0
+ ),
+ "vulnerabilityCountsByHost" AS (
+ SELECT
+ activeVulnerabilityInstalls.host,
+ COUNT(DISTINCT activeVulnerabilityInstalls.vulnerability) AS "vulnerabilityCount",
+ STRING_AGG(DISTINCT vulnerability."cveId", ', ' ORDER BY vulnerability."cveId") AS "cveIds"
+ FROM "activeVulnerabilityInstalls" AS activeVulnerabilityInstalls
+ JOIN vulnerability
+ ON vulnerability.id = activeVulnerabilityInstalls.vulnerability
+ GROUP BY
+ activeVulnerabilityInstalls.host
+ )
+ SELECT
+ host."displayName" AS "displayName",
+ host."fleetApid" AS "fleetApid",
+ vulnerabilityCountsByHost."vulnerabilityCount",
+ vulnerabilityCountsByHost."cveIds"
+ FROM "vulnerabilityCountsByHost" AS vulnerabilityCountsByHost
+ JOIN host
+ ON host.id = vulnerabilityCountsByHost.host
+ ORDER BY
+ vulnerabilityCountsByHost."vulnerabilityCount" DESC,
+ host."displayName" ASC
+ LIMIT 10;`;
+
+ } else {
+ hostsWithMostVulnsQuery = `
+ WITH activeVulnerabilityInstalls AS (
+ SELECT DISTINCT
+ vulnerabilityinstall.host,
+ vulnerabilityinstall.vulnerability
+ FROM vulnerabilityinstall
+ WHERE
+ vulnerabilityinstall.uninstalledAt IS NULL
+ OR vulnerabilityinstall.uninstalledAt = 0
+ ),
+ vulnerabilityCountsByHost AS (
+ SELECT
+ activeVulnerabilityInstalls.host,
+ COUNT(DISTINCT activeVulnerabilityInstalls.vulnerability) AS vulnerabilityCount,
+ GROUP_CONCAT(
+ DISTINCT vulnerability.cveId
+ ORDER BY vulnerability.cveId
+ SEPARATOR ', '
+ ) AS cveIds
+ FROM activeVulnerabilityInstalls
+ JOIN vulnerability
+ ON vulnerability.id = activeVulnerabilityInstalls.vulnerability
+ GROUP BY activeVulnerabilityInstalls.host
+ )
+ SELECT
+ host.displayName AS displayName,
+ host.fleetApid AS fleetApid,
+ vulnerabilityCountsByHost.vulnerabilityCount,
+ vulnerabilityCountsByHost.cveIds
+ FROM vulnerabilityCountsByHost
+ JOIN host
+ ON host.id = vulnerabilityCountsByHost.host
+ ORDER BY
+ vulnerabilityCountsByHost.vulnerabilityCount DESC,
+ host.displayName ASC
+ LIMIT 10;`;
+ }
+
+ // console.time('hosts with most vulnerabilities query');
+ let hostWithMostVulnsResult = await sails.sendNativeQuery(hostsWithMostVulnsQuery);
+ // console.timeEnd('hosts with most vulnerabilities query');
+
+ let hostsWithMostVulns = hostWithMostVulnsResult.rows.map((host)=>{
+ let cves = host.cveIds.split(', ').map((cveId)=>{
+ return {
+ name: cveId,
+ fleetUrl: `${sails.config.custom.fleetBaseUrl}/software/vulnerabilities/${cveId}`
+ };
+ });
+ return {
+ displayName: host.displayName,
+ fleetUrl: `${sails.config.custom.fleetBaseUrl}/hosts/${host.fleetApid}`,
+ numberOfVulns: host.vulnerabilityCount,
+ cves,
+ };
+ });
+
+ dataForGraphs.hostsWithMostVulns = hostsWithMostVulns;
+
+
+
+ //
+ // ╔═╗╦ ╦╔═╗ ╔═╗╔═╗╔╦╗╦╦ ╦╦╔╦╗╦ ╦ ╔╦╗╦═╗╔═╗╔╗╔╔╦╗╔═╗
+ // ║ ╚╗╔╝║╣ ╠═╣║ ║ ║╚╗╔╝║ ║ ╚╦╝ ║ ╠╦╝║╣ ║║║ ║║╚═╗
+ // ╚═╝ ╚╝ ╚═╝ ╩ ╩╚═╝ ╩ ╩ ╚╝ ╩ ╩ ╩ ╩ ╩╚═╚═╝╝╚╝═╩╝╚═╝
+ let queryToFindRemediationVeloticy;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ queryToFindRemediationVeloticy = `
+ WITH "params" AS (
+ SELECT
+ (3::bigint * 24 * 60 * 60 * 1000) AS "graceMs"
+ ),
+ "monthAnchors" AS (
+ SELECT date_trunc('month', (now() AT TIME ZONE 'UTC'))::timestamptz AS "thisMonthStartTs"
+ ),
+ "monthBoundaries" AS (
+
+ SELECT
+ gs AS "monthStartTs",
+ LEAD(gs) OVER (ORDER BY gs) AS "monthEndTs"
+ FROM generate_series(
+ (SELECT "thisMonthStartTs" - INTERVAL '4 months' FROM "monthAnchors"),
+ (SELECT "thisMonthStartTs" FROM "monthAnchors"),
+ INTERVAL '1 month'
+ ) AS gs
+ ),
+ "months" AS (
+
+ SELECT
+ (EXTRACT(EPOCH FROM "monthStartTs") * 1000)::bigint AS "monthStartMs",
+ (EXTRACT(EPOCH FROM "monthEndTs") * 1000)::bigint AS "monthEndMs"
+ FROM "monthBoundaries"
+ WHERE "monthEndTs" IS NOT NULL
+ ),
+ "rowsWithLag" AS (
+
+ SELECT
+ vulnerabilityinstall.host,
+ vulnerabilityinstall."softwareName",
+ vulnerabilityinstall."versionName",
+ vulnerabilityinstall.vulnerability,
+ vulnerabilityinstall."createdAt"::bigint AS "createdAtMs",
+ vulnerabilityinstall."uninstalledAt"::bigint AS "uninstalledAtMs",
+ LAG(vulnerabilityinstall."uninstalledAt"::bigint) OVER (
+ PARTITION BY
+ vulnerabilityinstall.host,
+ vulnerabilityinstall."softwareName",
+ vulnerabilityinstall."versionName",
+ vulnerabilityinstall.vulnerability
+ ORDER BY vulnerabilityinstall."createdAt"::bigint
+ ) AS "prevUninstalledMs"
+ FROM vulnerabilityinstall
+ ),
+ "labeledEpisodes" AS (
+ SELECT
+ "rowsWithLag".*,
+ CASE
+ WHEN "rowsWithLag"."prevUninstalledMs" IS NULL THEN 1
+ WHEN "rowsWithLag"."createdAtMs" > "rowsWithLag"."prevUninstalledMs" + (SELECT "graceMs" FROM "params") THEN 1
+ ELSE 0
+ END AS "isNewEpisode"
+ FROM "rowsWithLag"
+ ),
+ "episodeIndex" AS (
+
+ SELECT
+ host,
+ "softwareName",
+ "versionName",
+ vulnerability,
+ "createdAtMs",
+ "uninstalledAtMs",
+ SUM("isNewEpisode") OVER (
+ PARTITION BY host, "softwareName", "versionName", vulnerability
+ ORDER BY "createdAtMs"
+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+ ) AS "episodeId"
+ FROM "labeledEpisodes"
+ ),
+ "episodes" AS (
+
+ SELECT
+ host,
+ "softwareName",
+ "versionName",
+ vulnerability,
+ MIN("createdAtMs") AS "episodeStartMs",
+ NULLIF(MAX(COALESCE("uninstalledAtMs", 0)), 0) AS "episodeEndMs"
+ FROM "episodeIndex"
+ GROUP BY host, "softwareName", "versionName", vulnerability, "episodeId"
+ ),
+ "newCvesByMonth" AS (
+
+ SELECT
+ "months"."monthStartMs",
+ COUNT(*) AS "newCves"
+ FROM "months"
+ JOIN "episodes"
+ ON "episodes"."episodeStartMs" >= "months"."monthStartMs"
+ AND "episodes"."episodeStartMs" < "months"."monthEndMs"
+ GROUP BY "months"."monthStartMs"
+ ),
+ "remediatedCvesByMonth" AS (
+
+ SELECT
+ "months"."monthStartMs",
+ COUNT(*) AS "remediatedCves"
+ FROM "months"
+ JOIN "episodes"
+ ON "episodes"."episodeEndMs" IS NOT NULL
+ AND "episodes"."episodeEndMs" >= "months"."monthStartMs"
+ AND "episodes"."episodeEndMs" < "months"."monthEndMs"
+ GROUP BY "months"."monthStartMs"
+ ),
+ "activeCveCountByMonth" AS (
+
+ SELECT
+ "months"."monthStartMs",
+ COUNT(*) AS "activeCveCount"
+ FROM "months"
+ JOIN "episodes"
+ ON "episodes"."episodeStartMs" < "months"."monthEndMs"
+ AND (
+ "episodes"."episodeEndMs" IS NULL
+ OR "episodes"."episodeEndMs" >= "months"."monthEndMs"
+ )
+ GROUP BY "months"."monthStartMs"
+ )
+ SELECT
+ "months"."monthStartMs",
+ COALESCE("newCvesByMonth"."newCves", 0) AS "newCves",
+ COALESCE("remediatedCvesByMonth"."remediatedCves", 0) AS "remediatedCves",
+ COALESCE("activeCveCountByMonth"."activeCveCount", 0) AS "activeCveCount"
+ FROM "months"
+ LEFT JOIN "newCvesByMonth"
+ ON "newCvesByMonth"."monthStartMs" = "months"."monthStartMs"
+ LEFT JOIN "remediatedCvesByMonth"
+ ON "remediatedCvesByMonth"."monthStartMs" = "months"."monthStartMs"
+ LEFT JOIN "activeCveCountByMonth"
+ ON "activeCveCountByMonth"."monthStartMs" = "months"."monthStartMs"
+ ORDER BY "months"."monthStartMs" ASC;`;
+
+ } else {
+ queryToFindRemediationVeloticy = `
+ WITH
+ params AS (
+ SELECT CAST(3*24*60*60*1000 AS SIGNED) AS graceMs
+ ),
+ monthAnchors AS (
+
+ SELECT TIMESTAMP(DATE_FORMAT(UTC_TIMESTAMP(), '%Y-%m-01 00:00:00')) AS thisMonthStartTs
+ ),
+ monthBoundaries AS (
+
+ WITH RECURSIVE m AS (
+ SELECT
+ 0 AS n,
+ DATE_SUB((SELECT thisMonthStartTs FROM monthAnchors), INTERVAL 4 MONTH) AS monthStartTs
+ UNION ALL
+ SELECT
+ n + 1,
+ DATE_ADD(monthStartTs, INTERVAL 1 MONTH)
+ FROM m
+ WHERE n < 3
+ )
+ SELECT monthStartTs FROM m
+ ),
+ months AS (
+
+ SELECT
+ CAST(UNIX_TIMESTAMP(monthStartTs) * 1000 AS SIGNED) AS monthStartMs,
+ CAST(UNIX_TIMESTAMP(DATE_ADD(monthStartTs, INTERVAL 1 MONTH)) * 1000 AS SIGNED) AS monthEndMs
+ FROM monthBoundaries
+ ),
+ rowsWithLag AS (
+
+ SELECT
+ vulnerabilityinstall.host,
+ vulnerabilityinstall.softwareName,
+ vulnerabilityinstall.versionName,
+ vulnerabilityinstall.vulnerability,
+ CAST(vulnerabilityinstall.createdAt AS SIGNED) AS createdAtMs,
+ CAST(vulnerabilityinstall.uninstalledAt AS SIGNED) AS uninstalledAtMs,
+ LAG(CAST(vulnerabilityinstall.uninstalledAt AS SIGNED)) OVER (
+ PARTITION BY
+ vulnerabilityinstall.host,
+ vulnerabilityinstall.softwareName,
+ vulnerabilityinstall.versionName,
+ vulnerabilityinstall.vulnerability
+ ORDER BY CAST(vulnerabilityinstall.createdAt AS SIGNED)
+ ) AS prevUninstalledMs
+ FROM vulnerabilityinstall
+ ),
+ labeledEpisodes AS (
+ SELECT
+ rowsWithLag.*,
+ CASE
+ WHEN rowsWithLag.prevUninstalledMs IS NULL THEN 1
+ WHEN rowsWithLag.createdAtMs > rowsWithLag.prevUninstalledMs + (SELECT graceMs FROM params) THEN 1
+ ELSE 0
+ END AS isNewEpisode
+ FROM rowsWithLag
+ ),
+ episodeIndex AS (
+
+ SELECT
+ host,
+ softwareName,
+ versionName,
+ vulnerability,
+ createdAtMs,
+ uninstalledAtMs,
+ SUM(isNewEpisode) OVER (
+ PARTITION BY host, softwareName, versionName, vulnerability
+ ORDER BY createdAtMs
+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+ ) AS episodeId
+ FROM labeledEpisodes
+ ),
+ episodes AS (
+
+ SELECT
+ host,
+ softwareName,
+ versionName,
+ vulnerability,
+ MIN(createdAtMs) AS episodeStartMs,
+ NULLIF(MAX(COALESCE(uninstalledAtMs, 0)), 0) AS episodeEndMs
+ FROM episodeIndex
+ GROUP BY host, softwareName, versionName, vulnerability, episodeId
+ ),
+ newCvesByMonth AS (
+
+ SELECT
+ months.monthStartMs,
+ COUNT(*) AS newCves
+ FROM months
+ JOIN episodes
+ ON episodes.episodeStartMs >= months.monthStartMs
+ AND episodes.episodeStartMs < months.monthEndMs
+ GROUP BY months.monthStartMs
+ ),
+ remediatedCvesByMonth AS (
+
+ SELECT
+ months.monthStartMs,
+ COUNT(*) AS remediatedCves
+ FROM months
+ JOIN episodes
+ ON episodes.episodeEndMs IS NOT NULL
+ AND episodes.episodeEndMs >= months.monthStartMs
+ AND episodes.episodeEndMs < months.monthEndMs
+ GROUP BY months.monthStartMs
+ ),
+ activeCveCountByMonth AS (
+
+ SELECT
+ months.monthStartMs,
+ COUNT(*) AS activeCveCount
+ FROM months
+ JOIN episodes
+ ON episodes.episodeStartMs < months.monthEndMs
+ AND (episodes.episodeEndMs IS NULL OR episodes.episodeEndMs >= months.monthEndMs)
+ GROUP BY months.monthStartMs
+ )
+ SELECT
+ months.monthStartMs,
+ COALESCE(newCvesByMonth.newCves, 0) AS newCves,
+ COALESCE(remediatedCvesByMonth.remediatedCves, 0) AS remediatedCves,
+ COALESCE(activeCveCountByMonth.activeCveCount, 0) AS activeCveCount
+ FROM months
+ LEFT JOIN newCvesByMonth
+ ON newCvesByMonth.monthStartMs = months.monthStartMs
+ LEFT JOIN remediatedCvesByMonth
+ ON remediatedCvesByMonth.monthStartMs = months.monthStartMs
+ LEFT JOIN activeCveCountByMonth
+ ON activeCveCountByMonth.monthStartMs = months.monthStartMs
+ ORDER BY months.monthStartMs ASC;`;
+ }
+
+ // console.time('remediation velocity query');
+ let remediationTrendResult = await sails.sendNativeQuery(queryToFindRemediationVeloticy);
+ // console.timeEnd('remediation velocity query');
+
+ let activityTrendsTimeline = remediationTrendResult.rows.map((month)=>{
+ return {
+ monthStartedAt: month.monthStartMs,
+ timelineLabel: new Date(Number(month.monthStartMs)).toLocaleString('en-US', { month: 'short', year: 'numeric' }),
+ newCves: month.newCves,
+ remediatedCves: month.remediatedCves,
+ totalNumberOfCves: month.activeCveCount,
+ };
+ });
+ dataForGraphs.activityTrendsTimeline = activityTrendsTimeline;
+
+
+
+ //
+ // ╔═╗╦ ╦╔═╗ ╦═╗╔═╗╔╦╗╔═╗╔╦╗╦╔═╗╔╦╗╦╔═╗╔╗╔ ╔═╗╦ ╦╔╦╗╔╦╗╔═╗╦═╗╦ ╦
+ // ║ ╚╗╔╝║╣ ╠╦╝║╣ ║║║║╣ ║║║╠═╣ ║ ║║ ║║║║ ╚═╗║ ║║║║║║║╠═╣╠╦╝╚╦╝
+ // ╚═╝ ╚╝ ╚═╝ ╩╚═╚═╝╩ ╩╚═╝═╩╝╩╩ ╩ ╩ ╩╚═╝╝╚╝ ╚═╝╚═╝╩ ╩╩ ╩╩ ╩╩╚═ ╩
+ let thirtyDaysInMs = (1000 * 60 * 60 * 24 * 30);
+ let ninetyDaysInMs = (1000 * 60 * 60 * 24 * 90);
+ let thirtyDaysAgoAt = Date.now() - thirtyDaysInMs;
+ let ninetyDaysAgoAt = Date.now() - ninetyDaysInMs;
+
+ // let numberOfNewCvesInThePastWeek = await Vulnerability.count({createdAt: {'>': oneWeekAgoAt}});
+ // console.log('numberOfNewCvesInThePastWeek:', numberOfNewCvesInThePastWeek);
+ let numberOfNewAndActiveVulnInstallsInThePastThirtyDays = await VulnerabilityInstall.count({installedAt: {'>': thirtyDaysAgoAt}, uninstalledAt: 0});
+ // console.log('numberOfNewAndActiveVulnInstallsInThePastWeek:', numberOfNewAndActiveVulnInstallsInThePastWeek);
+ // let numberOfNewCvesInThePastThirtyDays = await Vulnerability.count({createdAt: {'>': thirtyDaysAgoAt}});
+ let numberOfNewAndActiveVulnInstallsInThePastNinetyDays = await VulnerabilityInstall.count({installedAt: {'>': ninetyDaysAgoAt}, uninstalledAt: 0});
+ // console.log('numberOfNewCvesInThePastThirtyDays:', numberOfNewCvesInThePastThirtyDays):
+ // console.log('numberOfNewAndActiveVulnInstallsInThePastThirtyDays:', numberOfNewAndActiveVulnInstallsInThePastThirtyDays);
+ let remediationSummary = {
+ newCvesThirtyDays: numberOfNewAndActiveVulnInstallsInThePastThirtyDays,
+ newCvesNinetyDays: numberOfNewAndActiveVulnInstallsInThePastNinetyDays,
+ };
+
+ // Send native queries to get number of remediated CVEs
+ let nativeQueryToGetNumberOfRemediatedCves;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ nativeQueryToGetNumberOfRemediatedCves = `
+ WITH "timeBounds" AS (
+ SELECT
+ (EXTRACT(EPOCH FROM (now() AT TIME ZONE 'UTC')) * 1000)::bigint AS "nowMs",
+ (EXTRACT(EPOCH FROM ((now() AT TIME ZONE 'UTC') - INTERVAL '30 days')) * 1000)::bigint AS "thirtyDaysAgoMs",
+ (EXTRACT(EPOCH FROM ((now() AT TIME ZONE 'UTC') - INTERVAL '90 days')) * 1000)::bigint AS "ninetyDaysAgoMs"
+ ),
+
+ "remediatedOnceLast90Days" AS (
+
+ SELECT
+ vulnerabilityinstall.id,
+ vulnerabilityinstall."uninstalledAt" AS "remediatedAtMs"
+ FROM vulnerabilityinstall
+ JOIN "timeBounds" ON TRUE
+ WHERE
+ vulnerabilityinstall."uninstalledAt" IS NOT NULL
+ AND vulnerabilityinstall."uninstalledAt" > 0
+ AND vulnerabilityinstall."uninstalledAt" >= "timeBounds"."ninetyDaysAgoMs"
+ AND vulnerabilityinstall."uninstalledAt" < "timeBounds"."nowMs"
+ AND NOT EXISTS (
+ SELECT 1
+ FROM vulnerabilityinstall AS vi_reinstall
+ WHERE
+ vi_reinstall.host = vulnerabilityinstall.host
+ AND vi_reinstall.vulnerability = vulnerabilityinstall.vulnerability
+ AND vi_reinstall."softwareName" = vulnerabilityinstall."softwareName"
+ AND COALESCE(vi_reinstall."versionName",'') = COALESCE(vulnerabilityinstall."versionName",'')
+ AND vi_reinstall."createdAt" > vulnerabilityinstall."uninstalledAt"
+ )
+ )
+
+ SELECT
+ COUNT(*) FILTER (WHERE "remediatedAtMs" >= "timeBounds"."thirtyDaysAgoMs") AS "remediatedCveCountLast30Days",
+ COUNT(*) AS "remediatedCveCountLast90Days"
+ FROM "remediatedOnceLast90Days"
+ CROSS JOIN "timeBounds";`;
+
+ } else {
+ nativeQueryToGetNumberOfRemediatedCves = `
+ WITH timeBounds AS (
+ SELECT
+ CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP()) * 1000 AS SIGNED) AS nowMs,
+ CAST(UNIX_TIMESTAMP(DATE_SUB(UTC_TIMESTAMP(), INTERVAL 7 DAY)) * 1000 AS SIGNED) AS thirtyDaysAgoMs,
+ CAST(UNIX_TIMESTAMP(DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY)) * 1000 AS SIGNED) AS ninetyDaysAgoMs
+ ),
+ remediatedOnceLast90Days AS (
+
+ SELECT
+ vulnerabilityinstall.id,
+ vulnerabilityinstall.uninstalledAt AS remediatedAtMs
+ FROM vulnerabilityinstall
+ JOIN timeBounds ON TRUE
+ WHERE
+ vulnerabilityinstall.uninstalledAt IS NOT NULL
+ AND vulnerabilityinstall.uninstalledAt > 0
+ AND vulnerabilityinstall.uninstalledAt >= timeBounds.ninetyDaysAgoMs
+ AND vulnerabilityinstall.uninstalledAt < timeBounds.nowMs
+ AND NOT EXISTS (
+ SELECT 1
+ FROM vulnerabilityinstall AS vi_reinstall
+ WHERE
+ vi_reinstall.host = vulnerabilityinstall.host
+ AND vi_reinstall.vulnerability = vulnerabilityinstall.vulnerability
+ AND vi_reinstall.softwareName = vulnerabilityinstall.softwareName
+ AND COALESCE(vi_reinstall.versionName,'') = COALESCE(vulnerabilityinstall.versionName,'')
+ AND vi_reinstall.createdAt > vulnerabilityinstall.uninstalledAt
+ )
+ )
+ SELECT
+ SUM(CASE WHEN remediatedAtMs >= timeBounds.thirtyDaysAgoMs THEN 1 ELSE 0 END) AS remediatedCveCountLast30Days,
+ COUNT(*) AS remediatedCveCountLast90Days
+ FROM remediatedOnceLast90Days
+ CROSS JOIN timeBounds;`;
+ }
+
+ // console.time('recent remediated cves query');
+ let remediatedCvesQueryResult = await sails.sendNativeQuery(nativeQueryToGetNumberOfRemediatedCves);
+ // console.timeEnd('recent remediated cves query');
+ remediationSummary.remediatedCvesThirtyDays = remediatedCvesQueryResult.rows[0].remediatedCveCountLast30Days ? remediatedCvesQueryResult.rows[0].remediatedCveCountLast30Days : 0;
+ remediationSummary.remediatedCvesNinetyDays = remediatedCvesQueryResult.rows[0].remediatedCveCountLast90Days ? remediatedCvesQueryResult.rows[0].remediatedCveCountLast90Days : 0;
+
+
+ dataForGraphs.remediationSummary = remediationSummary;
+
+
+ //
+ // ╔═╗╦ ╔╦╗╔═╗╔═╗╔╦╗ ╔═╗╦ ╦╔═╗╔═╗ ╦ ╦╦╔╦╗╦ ╦ ╔═╗═╗ ╦╔═╗╦ ╔═╗╦╔╦╗╔═╗
+ // ║ ║║ ║║║╣ ╚═╗ ║ ║ ╚╗╔╝║╣ ╚═╗ ║║║║ ║ ╠═╣ ║╣ ╔╩╦╝╠═╝║ ║ ║║ ║ ╚═╗
+ // ╚═╝╩═╝═╩╝╚═╝╚═╝ ╩ ╚═╝ ╚╝ ╚═╝╚═╝ ╚╩╝╩ ╩ ╩ ╩ ╚═╝╩ ╚═╩ ╩═╝╚═╝╩ ╩ ╚═╝
+ let queryToFindOldestExploitableCves;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ queryToFindOldestExploitableCves = `
+ WITH oldestExploitable AS (
+ SELECT DISTINCT vulnerability."cveId", vulnerability."publishedAt"
+ FROM vulnerability
+ JOIN vulnerabilityinstall
+ ON vulnerabilityinstall.vulnerability = vulnerability.id
+ AND (vulnerabilityinstall."uninstalledAt" IS NULL OR vulnerabilityinstall."uninstalledAt" = 0)
+ WHERE vulnerability."hasKnownExploit" = TRUE
+ )
+ SELECT "cveId"
+ FROM oldestExploitable
+ ORDER BY "publishedAt" ASC
+ LIMIT 5;`;
+
+ } else {
+ queryToFindOldestExploitableCves = `
+ WITH oldestExploitable AS (
+ SELECT DISTINCT
+ vulnerability.cveId,
+ vulnerability.publishedAt
+ FROM vulnerability
+ JOIN vulnerabilityinstall
+ ON vulnerabilityinstall.vulnerability = vulnerability.id
+ AND (vulnerabilityinstall.uninstalledAt IS NULL
+ OR vulnerabilityinstall.uninstalledAt = 0)
+ WHERE vulnerability.hasKnownExploit = TRUE
+ )
+ SELECT cveId
+ FROM oldestExploitable
+ ORDER BY publishedAt ASC
+ LIMIT 5;`;
+ }
+
+ // console.time('oldest exploitable cves query');
+ let oldestExploitableCvesResult = await sails.sendNativeQuery(queryToFindOldestExploitableCves);
+ // console.timeEnd('oldest exploitable cves query');
+
+ let oldestExploitableCves = oldestExploitableCvesResult.rows.map((cve)=>{
+ return {
+ cveId: cve.cveId,
+ fleetUrl: `${sails.config.custom.fleetBaseUrl}/software/vulnerabilities/${cve.cveId}`,
+ yearPublished: cve.cveId.split('-')[1],
+ };
+ });
+ dataForGraphs.oldestExploitableCves = oldestExploitableCves;
+
+
+ //
+ // ╦ ╦╔═╗╔═╗╔╦╗╔═╗ ╦ ╦╦╔╦╗╦ ╦ ╔═╗╦ ╔╦╗╔═╗╔═╗╔╦╗ ╔═╗═╗ ╦╔═╗╦ ╔═╗╦╔╦╗╔═╗╔╗ ╦ ╔═╗ ╔═╗╦ ╦╔═╗╔═╗
+ // ╠═╣║ ║╚═╗ ║ ╚═╗ ║║║║ ║ ╠═╣ ║ ║║ ║║║╣ ╚═╗ ║ ║╣ ╔╩╦╝╠═╝║ ║ ║║ ║ ╠═╣╠╩╗║ ║╣ ║ ╚╗╔╝║╣ ╚═╗
+ // ╩ ╩╚═╝╚═╝ ╩ ╚═╝ ╚╩╝╩ ╩ ╩ ╩ ╚═╝╩═╝═╩╝╚═╝╚═╝ ╩ ╚═╝╩ ╚═╩ ╩═╝╚═╝╩ ╩ ╩ ╩╚═╝╩═╝╚═╝ ╚═╝ ╚╝ ╚═╝╚═╝
+ let queryToFindHostsWithOldestExploitableCves;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ queryToFindHostsWithOldestExploitableCves = `
+ WITH "hostOldestExploitable" AS (
+ SELECT
+ host.id AS "hostId",
+ host."displayName" AS "displayName",
+ host."fleetApid" AS "fleetApid",
+ vulnerability."cveId" AS "cveId",
+ vulnerability."publishedAt" AS "publishedAtMs",
+ vulnerabilityinstall."softwareName" AS "softwareName",
+ vulnerabilityinstall."versionName" AS "versionName"
+ FROM vulnerabilityinstall
+ JOIN vulnerability
+ ON vulnerability.id = vulnerabilityinstall.vulnerability
+ AND vulnerability."hasKnownExploit" = TRUE
+ JOIN host
+ ON host.id = vulnerabilityinstall.host
+ WHERE
+ vulnerabilityinstall."uninstalledAt" IS NULL
+ OR vulnerabilityinstall."uninstalledAt" = 0
+ ),
+ "rankedOldestPerHost" AS (
+ SELECT
+ "hostId",
+ "displayName",
+ "fleetApid",
+ "cveId",
+ "publishedAtMs",
+ "softwareName",
+ "versionName",
+ ROW_NUMBER() OVER (
+ PARTITION BY "hostId"
+ ORDER BY "publishedAtMs" ASC
+ ) AS "rowNumber"
+ FROM "hostOldestExploitable"
+ )
+ SELECT
+ "displayName",
+ "fleetApid",
+ "cveId",
+ ("softwareName" || ' ' || COALESCE("versionName", '')) AS "softwareNameAndVersion",
+ "publishedAtMs"
+ FROM "rankedOldestPerHost"
+ WHERE "rowNumber" = 1
+ ORDER BY "publishedAtMs" ASC
+ LIMIT 5;`;
+
+ } else {
+ queryToFindHostsWithOldestExploitableCves = `
+ WITH hostOldestExploitable AS (
+ SELECT
+ host.id AS hostId,
+ host.displayName AS displayName,
+ host.fleetApid AS fleetApid,
+ vulnerability.cveId AS cveId,
+ vulnerability.publishedAt AS publishedAtMs,
+ vulnerabilityinstall.softwareName AS softwareName,
+ vulnerabilityinstall.versionName AS versionName
+ FROM vulnerabilityinstall
+ JOIN vulnerability
+ ON vulnerability.id = vulnerabilityinstall.vulnerability
+ AND vulnerability.hasKnownExploit = TRUE
+ JOIN host
+ ON host.id = vulnerabilityinstall.host
+ WHERE
+ vulnerabilityinstall.uninstalledAt IS NULL
+ OR vulnerabilityinstall.uninstalledAt = 0
+ ),
+ rankedOldestPerHost AS (
+ SELECT
+ hostId,
+ displayName,
+ fleetApid,
+ cveId,
+ publishedAtMs,
+ softwareName,
+ versionName,
+ ROW_NUMBER() OVER (
+ PARTITION BY hostId
+ ORDER BY publishedAtMs ASC
+ ) AS rowNumber
+ FROM hostOldestExploitable
+ )
+ SELECT
+ displayName,
+ fleetApid,
+ cveId,
+ CONCAT(softwareName, ' ', COALESCE(versionName, '')) AS softwareNameAndVersion,
+ publishedAtMs
+ FROM rankedOldestPerHost
+ WHERE rowNumber = 1
+ ORDER BY publishedAtMs ASC
+ LIMIT 5;`;
+ }
+
+ // console.time('oldest exploitable hosts query');
+ let oldestExploitableHostsResult = await sails.sendNativeQuery(queryToFindHostsWithOldestExploitableCves);
+ // console.timeEnd('oldest exploitable hosts query');
+
+
+ let oldestExploitableHosts = oldestExploitableHostsResult.rows.map((host)=>{
+ return {
+ displayName: host.displayName,
+ affectedSoftware: host.softwareNameAndVersion,
+ fleetUrl: `${sails.config.custom.fleetBaseUrl}/hosts/${host.fleetApid}`,
+ cveId: host.cveId,
+ yearPublished: host.cveId.split('-')[1],
+ };
+ });
+
+ dataForGraphs.oldestExploitableHosts = oldestExploitableHosts;
+
+
+ //
+ // ╔╗╔╔═╗╦ ╦╔═╗╔═╗╔╦╗ ╔═╗╦ ╦╔═╗╔═╗ ╦ ╦╦╔╦╗╦ ╦ ╔═╗═╗ ╦╔═╗╦ ╔═╗╦╔╦╗╔═╗
+ // ║║║║╣ ║║║║╣ ╚═╗ ║ ║ ╚╗╔╝║╣ ╚═╗ ║║║║ ║ ╠═╣ ║╣ ╔╩╦╝╠═╝║ ║ ║║ ║ ╚═╗
+ // ╝╚╝╚═╝╚╩╝╚═╝╚═╝ ╩ ╚═╝ ╚╝ ╚═╝╚═╝ ╚╩╝╩ ╩ ╩ ╩ ╚═╝╩ ╚═╩ ╩═╝╚═╝╩ ╩ ╚═╝
+ let queryToFindNewestExploitableCves;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ queryToFindNewestExploitableCves = `
+ WITH newestExploitable AS (
+ SELECT DISTINCT vulnerability."cveId", vulnerability."publishedAt"
+ FROM vulnerability
+ JOIN vulnerabilityinstall
+ ON vulnerabilityinstall.vulnerability = vulnerability.id
+ AND (vulnerabilityinstall."uninstalledAt" IS NULL OR vulnerabilityinstall."uninstalledAt" = 0)
+ WHERE vulnerability."hasKnownExploit" = TRUE
+ )
+ SELECT "cveId"
+ FROM newestExploitable
+ ORDER BY "publishedAt" DESC
+ LIMIT 5;`;
+
+ } else {
+ queryToFindNewestExploitableCves = `
+ WITH newestExploitable AS (
+ SELECT DISTINCT
+ vulnerability.cveId,
+ vulnerability.publishedAt
+ FROM vulnerability
+ JOIN vulnerabilityinstall
+ ON vulnerabilityinstall.vulnerability = vulnerability.id
+ AND (vulnerabilityinstall.uninstalledAt IS NULL OR vulnerabilityinstall.uninstalledAt = 0)
+ WHERE vulnerability.hasKnownExploit = TRUE
+ )
+ SELECT cveId
+ FROM newestExploitable
+ ORDER BY publishedAt DESC
+ LIMIT 5;`;
+ }
+
+ // console.time('newest exploitable cves');
+ let newestExploitableCvesResult = await sails.sendNativeQuery(queryToFindNewestExploitableCves);
+ // console.timeEnd('newest exploitable cves');
+ // console.log(newestExploitableCvesResult);
+ let newestExploitableCves = newestExploitableCvesResult.rows.map((cve)=>{
+ return {
+ cveId: cve.cveId,
+ fleetUrl: `${sails.config.custom.fleetBaseUrl}/software/vulnerabilities/${cve.cveId}`,
+ yearPublished: cve.cveId.split('-')[1],
+ };
+ });
+
+ dataForGraphs.newestExploitableCves = newestExploitableCves;
+
+ //
+ // ╦ ╦╔═╗╔═╗╔╦╗╔═╗ ╦ ╦╦╔╦╗╦ ╦ ╔╗╔╔═╗╦ ╦╔═╗╔═╗╔╦╗ ╔═╗═╗ ╦╔═╗╦ ╔═╗╦╔╦╗╔═╗╔╗ ╦ ╔═╗ ╔═╗╦ ╦╔═╗╔═╗
+ // ╠═╣║ ║╚═╗ ║ ╚═╗ ║║║║ ║ ╠═╣ ║║║║╣ ║║║║╣ ╚═╗ ║ ║╣ ╔╩╦╝╠═╝║ ║ ║║ ║ ╠═╣╠╩╗║ ║╣ ║ ╚╗╔╝║╣ ╚═╗
+ // ╩ ╩╚═╝╚═╝ ╩ ╚═╝ ╚╩╝╩ ╩ ╩ ╩ ╝╚╝╚═╝╚╩╝╚═╝╚═╝ ╩ ╚═╝╩ ╚═╩ ╩═╝╚═╝╩ ╩ ╩ ╩╚═╝╩═╝╚═╝ ╚═╝ ╚╝ ╚═╝╚═╝
+ let queryToFindHostsWithNewestExploitableCves;
+ if(sails.config.datastores.default.adapter === 'sails-postgresql') {
+ queryToFindHostsWithNewestExploitableCves = `
+ WITH "hostNewestExploitable" AS (
+ SELECT
+ host.id AS "hostId",
+ host."displayName" AS "displayName",
+ host."fleetApid" AS "fleetApid",
+ vulnerability."cveId" AS "cveId",
+ vulnerability."publishedAt" AS "publishedAtMs",
+ vulnerabilityinstall."softwareName" AS "softwareName",
+ vulnerabilityinstall."versionName" AS "versionName"
+ FROM vulnerabilityinstall
+ JOIN vulnerability
+ ON vulnerability.id = vulnerabilityinstall.vulnerability
+ AND vulnerability."hasKnownExploit" = TRUE
+ JOIN host
+ ON host.id = vulnerabilityinstall.host
+ WHERE
+ vulnerabilityinstall."uninstalledAt" IS NULL
+ OR vulnerabilityinstall."uninstalledAt" = 0
+ ),
+ "rankedNewestPerHost" AS (
+ SELECT
+ "hostId",
+ "displayName",
+ "fleetApid",
+ "cveId",
+ "publishedAtMs",
+ "softwareName",
+ "versionName",
+ ROW_NUMBER() OVER (
+ PARTITION BY "hostId"
+ ORDER BY "publishedAtMs" ASC
+ ) AS "rowNumber"
+ FROM "hostNewestExploitable"
+ )
+ SELECT
+ "displayName",
+ "fleetApid",
+ "cveId",
+ ("softwareName" || ' ' || COALESCE("versionName", '')) AS "softwareNameAndVersion",
+ "publishedAtMs"
+ FROM "rankedNewestPerHost"
+ WHERE "rowNumber" = 1
+ ORDER BY "publishedAtMs" DESC
+ LIMIT 5;`;
+ } else {
+ queryToFindHostsWithNewestExploitableCves = `
+ WITH hostNewestExploitable AS (
+ SELECT
+ host.id AS hostId,
+ host.displayName AS displayName,
+ host.fleetApid AS fleetApid,
+ vulnerability.cveId AS cveId,
+ vulnerability.publishedAt AS publishedAtMs,
+ vulnerabilityinstall.softwareName AS softwareName,
+ vulnerabilityinstall.versionName AS versionName
+ FROM vulnerabilityinstall
+ JOIN vulnerability
+ ON vulnerability.id = vulnerabilityinstall.vulnerability
+ AND vulnerability.hasKnownExploit = TRUE
+ JOIN host
+ ON host.id = vulnerabilityinstall.host
+ WHERE
+ vulnerabilityinstall.uninstalledAt IS NULL
+ OR vulnerabilityinstall.uninstalledAt = 0
+ ),
+ rankedNewestPerHost AS (
+ SELECT
+ hostId,
+ displayName,
+ fleetApid,
+ cveId,
+ publishedAtMs,
+ softwareName,
+ versionName,
+ ROW_NUMBER() OVER (
+ PARTITION BY hostId
+ ORDER BY publishedAtMs ASC
+ ) AS rowNumber
+ FROM hostNewestExploitable
+ )
+ SELECT
+ displayName,
+ fleetApid,
+ cveId,
+ CONCAT(softwareName, ' ', COALESCE(versionName, '')) AS softwareNameAndVersion,
+ publishedAtMs
+ FROM rankedNewestPerHost
+ WHERE rowNumber = 1
+ ORDER BY publishedAtMs DESC
+ LIMIT 5;`;
+ }
+
+ // console.time('hosts with newest exploitable cves query');
+ let newestExploitableHostsResult = await sails.sendNativeQuery(queryToFindHostsWithNewestExploitableCves);
+ // console.timeEnd('hosts with newest exploitable cves query');
+
+ // console.log(newestExploitableHostsResult);
+ let newestExploitableHosts = newestExploitableHostsResult.rows.map((host)=>{
+ return {
+ displayName: host.displayName,
+ affectedSoftware: host.softwareNameAndVersion,
+ fleetUrl: `${sails.config.custom.fleetBaseUrl}/hosts/${host.fleetApid}`,
+ cveId: host.cveId,
+ yearPublished: host.cveId.split('-')[1],
+ };
+ });
+
+ dataForGraphs.newestExploitableHosts = newestExploitableHosts;
+
+ // console.timeEnd(`Dashboard page view action`);
+ // Respond with view.
+ return {
+ ...dataForGraphs
+ };
+
+ }
+
+
+};
diff --git a/ee/vulnerability-dashboard/api/models/VulnerabilityInstall.js b/ee/vulnerability-dashboard/api/models/VulnerabilityInstall.js
index bb6d4aabb8..212d6cb2ac 100644
--- a/ee/vulnerability-dashboard/api/models/VulnerabilityInstall.js
+++ b/ee/vulnerability-dashboard/api/models/VulnerabilityInstall.js
@@ -15,6 +15,7 @@ module.exports = {
installedAt: {
example: 1670152500000,
description: 'JS timestamp representing when this installation began on the host.',
+ extendedDescription: 'This JS timestamp represents when the vulnerabiltiy dashboard first saw this vulnerable software on a host',
type: 'number',
isInteger: true,
required: true,
diff --git a/ee/vulnerability-dashboard/assets/js/pages/dashboard.page.js b/ee/vulnerability-dashboard/assets/js/pages/dashboard.page.js
new file mode 100644
index 0000000000..5b0457055b
--- /dev/null
+++ b/ee/vulnerability-dashboard/assets/js/pages/dashboard.page.js
@@ -0,0 +1,156 @@
+parasails.registerPage('dashboard', {
+ // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
+ // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
+ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
+ data: {
+ //…
+ },
+
+ // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
+ // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
+ // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
+ beforeMount: function() {
+ //…
+ },
+ mounted: async function() {
+ Chart.defaults.color = '#d4d4d8';
+ Chart.defaults.borderColor = '#21262d';
+ new Chart(document.getElementById('severityChart'), {
+ type: 'doughnut',
+ data: {
+ labels: ['Critical', 'High', 'Medium', 'Low'],
+ datasets: [{
+ data: [this.totalUniqueCounts.critical, this.totalUniqueCounts.high, this.totalUniqueCounts.medium, this.totalUniqueCounts.low],
+ backgroundColor: ['#dc2626', '#ea580c', '#ca8a04', '#16a34a'],
+ borderWidth: 2,
+ borderColor: '#010409'
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ padding: 20,
+ usePointStyle: true,
+ color: '#d4d4d8'
+ }
+ }
+ }
+ }
+ });
+ new Chart(document.getElementById('trendsChart'), {
+ type: 'line',
+ data: {
+ // labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'],
+ labels: _.pluck(this.activityTrendsTimeline, 'timelineLabel'),
+ datasets: [{
+ label: 'New CVEs',
+ data: _.pluck(this.activityTrendsTimeline, 'newCves'),
+ borderColor: '#dc2626',
+ backgroundColor: 'rgba(220, 38, 38, 0.1)',
+ tension: 0.4,
+ borderWidth: 2,
+ pointBackgroundColor: '#dc2626',
+ pointBorderColor: '#ffffff',
+ pointBorderWidth: 2,
+ pointRadius: 5
+ }, {
+ label: 'Remediated CVEs',
+ data: _.pluck(this.activityTrendsTimeline, 'remediatedCves'),
+ borderColor: '#16a34a',
+ backgroundColor: 'rgba(22, 163, 74, 0.1)',
+ tension: 0.4,
+ borderWidth: 2,
+ pointBackgroundColor: '#16a34a',
+ pointBorderColor: '#ffffff',
+ pointBorderWidth: 2,
+ pointRadius: 5
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: { color: '#21262d' },
+ ticks: { color: '#7d8590' }
+ },
+ x: {
+ grid: { color: '#21262d' },
+ ticks: { color: '#7d8590' }
+ }
+ },
+ plugins: {
+ legend: {
+ labels: {
+ color: '#d4d4d8',
+ usePointStyle: true,
+ padding: 20
+ }
+ }
+ }
+ }
+ });
+
+ new Chart(document.getElementById('totalTrendChart'), {
+ type: 'line',
+ data: {
+ labels: _.pluck(this.activityTrendsTimeline, 'timelineLabel'),
+ datasets: [{
+ label: 'Total CVEs',
+ data: _.pluck(this.activityTrendsTimeline, 'totalNumberOfCves'),
+ borderColor: '#8b5cf6',
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
+ tension: 0.4,
+ borderWidth: 3,
+ pointBackgroundColor: '#8b5cf6',
+ pointBorderColor: '#ffffff',
+ pointBorderWidth: 2,
+ pointRadius: 6,
+ fill: true
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: {
+ beginAtZero: false,
+ min: 25000,
+ grid: { color: '#21262d' },
+ ticks: {
+ color: '#7d8590',
+ callback: function(value) {
+ return (value / 1000).toFixed(1) + 'k';
+ }
+ }
+ },
+ x: {
+ grid: { color: '#21262d' },
+ ticks: { color: '#7d8590' }
+ }
+ },
+ plugins: {
+ legend: {
+ labels: {
+ color: '#d4d4d8',
+ usePointStyle: true,
+ padding: 20
+ }
+ }
+ }
+ }
+ });
+ },
+
+ // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
+ // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
+ // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
+ methods: {
+ //…
+ }
+});
diff --git a/ee/vulnerability-dashboard/assets/styles/importer.less b/ee/vulnerability-dashboard/assets/styles/importer.less
index fa31fee6ed..f29a3e0fd1 100644
--- a/ee/vulnerability-dashboard/assets/styles/importer.less
+++ b/ee/vulnerability-dashboard/assets/styles/importer.less
@@ -44,3 +44,4 @@
@import 'pages/500.less';
@import 'pages/498.less';
@import 'pages/patch-progress.less';
+@import 'pages/dashboard.less';
diff --git a/ee/vulnerability-dashboard/assets/styles/layout.less b/ee/vulnerability-dashboard/assets/styles/layout.less
index e73071a319..67eaf663fe 100644
--- a/ee/vulnerability-dashboard/assets/styles/layout.less
+++ b/ee/vulnerability-dashboard/assets/styles/layout.less
@@ -40,7 +40,11 @@ html, body {
// ^^The above is to disable "importantRule" and "duplicateProperty" rules.
min-height: 100%;
position: relative;
+
padding-bottom: @footer-height;
+ &.header-hidden {
+ padding-bottom: 0px;
+ }
background-color: #f8f9fa;
color: #292b2d;
a {
diff --git a/ee/vulnerability-dashboard/assets/styles/pages/dashboard.less b/ee/vulnerability-dashboard/assets/styles/pages/dashboard.less
new file mode 100644
index 0000000000..1fa4d9de37
--- /dev/null
+++ b/ee/vulnerability-dashboard/assets/styles/pages/dashboard.less
@@ -0,0 +1,280 @@
+#dashboard {
+ background: #111111;
+
+ .dashboard-container {
+ max-width: 1600px;
+ margin: 0 auto;
+ background: #111111;
+ border-radius: 12px;
+ padding: 30px;
+ // border: 1px solid #1f1f1f;
+ }
+
+ .dashboard-header {
+ text-align: center;
+ margin-bottom: 40px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid #1f1f1f;
+ }
+
+ .dashboard-links {
+ a {
+
+ color: #FFF;
+ &::hover {
+ color: #FFF;
+ }
+ }
+ }
+
+ .dashboard-title {
+ font-size: 2.2em;
+ font-weight: 300;
+ color: #ffffff;
+ margin-bottom: 8px;
+ letter-spacing: -0.5px;
+ }
+
+ .dashboard-subtitle {
+ color: #7d8590;
+ font-size: 0.9em;
+ font-weight: 300;
+ }
+
+ .metrics-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+ }
+
+ .main-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 25px;
+ margin-bottom: 30px;
+ }
+
+ .chart-row {
+ display: grid;
+ // grid-template-columns: 1fr 1fr;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 25px;
+ margin-bottom: 30px;
+ }
+
+ .full-width {
+ grid-column: 1 / -1;
+ }
+
+ .panel {
+ background: #0d1117;
+ border-radius: 8px;
+ padding: 24px;
+ border: 1px solid #1f2328;
+ transition: all 0.2s ease;
+ min-width: 0px;
+ }
+
+ .panel:hover {
+ border-color: #2563eb;
+ background: #161b22;
+ }
+
+ .metric-card {
+ background: #0d1117;
+ border-radius: 8px;
+ padding: 20px;
+ border: 1px solid #1f2328;
+ text-align: center;
+ transition: all 0.2s ease;
+ }
+
+ .metric-card:hover {
+ border-color: #2563eb;
+ transform: translateY(-2px);
+ }
+
+ .metric-value {
+ font-size: 2.5em;
+ font-weight: 300;
+ color: #ffffff;
+ line-height: 1;
+ margin-bottom: 8px;
+ }
+
+ .metric-label {
+ font-size: 0.85em;
+ color: #7d8590;
+ font-weight: 300;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .severity-critical { color: #ef4444; }
+ .severity-high { color: #f97316; }
+ .severity-medium { color: #eab308; }
+ .severity-low { color: #22c55e; }
+
+ .panel-title {
+ font-size: 1.1em;
+ font-weight: 400;
+ color: #ffffff;
+ margin-bottom: 20px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #1f2328;
+ }
+
+ .host-list {
+ list-style: none;
+ padding-inline-start: 0px;
+ }
+
+ .host-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #1f2328;
+ font-weight: 300;
+ }
+
+ .host-item:last-child {
+ border-bottom: none;
+ }
+
+ .host-name {
+ color: #d4d4d8;
+ font-size: 0.9em;
+ a {
+ color: unset;
+ }
+ }
+
+ .vulnerability-score {
+ background: #ef4444;
+ color: white;
+ padding: 4px 10px;
+ border-radius: 12px;
+ font-size: 0.8em;
+ font-weight: 400;
+ }
+
+ .software-list {
+ list-style: none;
+ padding-inline-start: 0px;
+ }
+
+ .software-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 0;
+ border-bottom: 1px solid #1f2328;
+ }
+
+ .software-item:last-child {
+ border-bottom: none;
+ }
+
+ .software-name {
+ color: #d4d4d8;
+ font-size: 0.9em;
+ font-weight: 300;
+ a {
+ color: unset;
+ }
+ }
+
+ .vuln-count {
+ background: #21262d;
+ color: #d4d4d8;
+ padding: 3px 8px;
+ border-radius: 8px;
+ font-size: 0.8em;
+ white-space: nowrap;
+ }
+
+ .chart-container {
+ position: relative;
+ height: 300px;
+ background: #010409;
+ border-radius: 6px;
+ padding: 15px;
+ border: 1px solid #0d1117;
+ }
+
+ .status-indicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ display: inline-block;
+ margin-right: 8px;
+ }
+
+ .remediation-summary {
+ margin-bottom: 30px;
+ }
+
+ .remediation-metrics {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20px;
+ text-align: center;
+ }
+
+
+ .status-external { background: #ef4444; }
+
+ @media (max-width: 1200px) {
+ .chart-panel {
+ grid-column: 1 / span 2;
+ }
+ }
+ @media (max-width: 991px) {
+ .main-grid {
+ display: flex;
+ flex-direction: column;
+ }
+ .metrics-row {
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ }
+
+ .chart-row {
+ grid-template-columns: 1fr;
+ }
+ }
+ @media (max-width: 768px) {
+ // .main-grid {
+ // grid-template-columns: 1fr;
+ // }
+
+ .chart-row {
+ grid-template-columns: 1fr;
+ }
+
+
+
+ .dashboard-container {
+ padding: 20px;
+ padding-bottom: 40px;
+ }
+ }
+ @media (max-width: 576px) {
+ .main-grid {
+ grid-template-columns: 1fr;
+ }
+ .remediation-metrics {
+ display: flex;
+ flex-direction: column;
+ }
+ .metrics-row {
+ display: flex;
+ flex-direction: column;
+ }
+
+ }
+
+
+
+}
diff --git a/ee/vulnerability-dashboard/config/routes.js b/ee/vulnerability-dashboard/config/routes.js
index 1df84416ba..a71afc2992 100644
--- a/ee/vulnerability-dashboard/config/routes.js
+++ b/ee/vulnerability-dashboard/config/routes.js
@@ -14,7 +14,7 @@ module.exports.routes = {
// ║║║║╣ ╠╩╗╠═╝╠═╣║ ╦║╣ ╚═╗
// ╚╩╝╚═╝╚═╝╩ ╩ ╩╚═╝╚═╝╚═╝
// 'GET /': { action: 'view-homepage-or-redirect' },
- 'GET /dashboard': { action: 'dashboard/view-welcome' },
+ // 'GET /dashboard': { action: 'dashboard/view-welcome' },
'GET /vulnerability-list': { action: 'dashboard/view-vulnerability-list' },
'GET /patch-progress': { action: 'view-patch-progress' },
@@ -35,7 +35,12 @@ module.exports.routes = {
'GET /account': { action: 'account/view-account-overview' },
'GET /account/password': { action: 'account/view-edit-password' },
'GET /account/profile': { action: 'account/view-edit-profile' },
-
+ 'GET /dashboard': {
+ action: 'view-dashboard',
+ locals: {
+ headerHidden: true,
+ }
+ },
// ╔╦╗╦╔═╗╔═╗ ╦═╗╔═╗╔╦╗╦╦═╗╔═╗╔═╗╔╦╗╔═╗ ┬ ╔╦╗╔═╗╦ ╦╔╗╔╦ ╔═╗╔═╗╔╦╗╔═╗
// ║║║║╚═╗║ ╠╦╝║╣ ║║║╠╦╝║╣ ║ ║ ╚═╗ ┌┼─ ║║║ ║║║║║║║║ ║ ║╠═╣ ║║╚═╗
diff --git a/ee/vulnerability-dashboard/views/layouts/layout.ejs b/ee/vulnerability-dashboard/views/layouts/layout.ejs
index 53be092a9c..d0efeda43a 100644
--- a/ee/vulnerability-dashboard/views/layouts/layout.ejs
+++ b/ee/vulnerability-dashboard/views/layouts/layout.ejs
@@ -2,6 +2,7 @@
// In case we're displaying the 404 or 500 page and relevant code in the "custom" hook was not able to run,
// we make sure `me` exists. This ensures we don't have to do `typeof` checks below.
var me = undefined;
+ var headerHidden;
}
let replaceBuiltInAuthWithOkta = (sails.config.custom.oktaClientSecret !== undefined);
if(typeof replaceBuiltInAuthWithOkta === 'undefined') {
@@ -53,7 +54,8 @@
-
+
+ <% } %>
<%- body %>
@@ -151,6 +154,7 @@
+
diff --git a/ee/vulnerability-dashboard/views/pages/dashboard.ejs b/ee/vulnerability-dashboard/views/pages/dashboard.ejs
new file mode 100644
index 0000000000..8be898547f
--- /dev/null
+++ b/ee/vulnerability-dashboard/views/pages/dashboard.ejs
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
{{(totalUniqueCounts.critical).toLocaleString()}}
+
Unique critical
+
+
+
{{(totalUniqueCounts.high).toLocaleString()}}
+
Unique high
+
+
+
{{(totalUniqueCounts.medium).toLocaleString()}}
+
Unique medium
+
+
+
{{(totalUniqueCounts.low).toLocaleString()}}
+
Unique low
+
+
+
+
+
+
{{(totalNumberOfVulns.critical).toLocaleString()}}
+
Total critical
+
+
+
{{(totalNumberOfVulns.high).toLocaleString()}}
+
Total high
+
+
+
{{(totalNumberOfVulns.medium).toLocaleString()}}
+
Total medium
+
+
+
{{(totalNumberOfVulns.low).toLocaleString()}}
+
Total low
+
+
+
+
+
+
The riskiest hosts right now
+
Host risk scores range from 0-10. The score weighs the number and severity of CVEs, exploit availability (public exploits double the weight). A "10" means urgent, drop-everything action: critical vulnerabilities with known exploits and high exposure.
+
+
+
+
+
The usual suspects in software
+
+
+
+
+
What percentage are critical?
+
+
+
+
+
+
+
+
Who installed this? (Critically rare software)
+
+ -
+
+ {{software.softwareNameAndVersion}}
+ {{software.numberOfVulns}} critical
+
+
+
+
+
Hosts with most CVEs
+
+
+
+
+
+
+
CVE activity trends
+
+
+
+
+
+
+
Total CVE count trend
+
+
+
+
+
+
+
+
+
+
+
+
Top 5 oldest CVEs with exploits
+
+
+
+
+
Top 5 hosts with oldest exploitable CVEs
+
+
+
+
+
Top 5 newest CVEs with exploits
+
+
+
+
+
Top 5 hosts with newest exploitable CVEs
+
+
+
+
+
+<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>