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 @@ -
+
+ <%if(!headerHidden) {%>
+ <% } %> <%- 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 @@ +
+
+
+

Lets get to a better state

+

Last Updated:

+ +
+ +
+
+
{{(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

+
+ +
+
+
+ +
+

CVE remediation summary (past 90 Days)

+
+
+
+{{remediationSummary.newCvesNinetyDays}}
+
New CVEs (90d)
+
+
+
-{{remediationSummary.remediatedCvesNinetyDays}}
+
Remediated (90d)
+
+
+
+{{remediationSummary.newCvesThirtyDays}}
+
New CVEs (30d)
+
+
+
-{{remediationSummary.remediatedCvesThirtyDays}}
+
Remediated (30d)
+
+
+
+ + +
+
+

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() %>