From 9e04caf0da61f5906193851feccb4936d956f0aa Mon Sep 17 00:00:00 2001 From: Holly Gong Date: Tue, 19 Nov 2024 16:52:38 +1100 Subject: [PATCH] feat(output): show package view for container scanning table result --- .../__snapshots__/output_result_test.snap | 96 +++++++++++++++++-- internal/output/output_result.go | 22 ++++- internal/output/table.go | 63 +++++++++++- 3 files changed, 166 insertions(+), 15 deletions(-) diff --git a/internal/output/__snapshots__/output_result_test.snap b/internal/output/__snapshots__/output_result_test.snap index e1b7318511..fdd3eec7e4 100755 --- a/internal/output/__snapshots__/output_result_test.snap +++ b/internal/output/__snapshots__/output_result_test.snap @@ -256,9 +256,13 @@ "IsContainerScanning": false, "AllLayers": [], "VulnTypeCount": { - "All": 4, + "All": 6, "OS": 0, - "Project": 4, + "Project": 6, + "Uncalled": 0 + }, + "PackageTypeCount": { + "Called": 4, "Uncalled": 0 }, "VulnCount": { @@ -539,9 +543,13 @@ "IsContainerScanning": false, "AllLayers": [], "VulnTypeCount": { - "All": 4, + "All": 6, "OS": 0, - "Project": 4, + "Project": 6, + "Uncalled": 0 + }, + "PackageTypeCount": { + "Called": 4, "Uncalled": 0 }, "VulnCount": { @@ -814,6 +822,10 @@ "Project": 0, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 0, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 0, @@ -1120,6 +1132,10 @@ "Project": 3, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 3, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 3, @@ -1404,9 +1420,13 @@ "IsContainerScanning": false, "AllLayers": [], "VulnTypeCount": { - "All": 4, + "All": 6, "OS": 0, - "Project": 4, + "Project": 6, + "Uncalled": 0 + }, + "PackageTypeCount": { + "Called": 4, "Uncalled": 0 }, "VulnCount": { @@ -1694,9 +1714,13 @@ "IsContainerScanning": false, "AllLayers": [], "VulnTypeCount": { - "All": 4, + "All": 5, "OS": 0, - "Project": 4, + "Project": 5, + "Uncalled": 1 + }, + "PackageTypeCount": { + "Called": 4, "Uncalled": 1 }, "VulnCount": { @@ -1816,6 +1840,10 @@ "Project": 0, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 0, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 0, @@ -1848,6 +1876,10 @@ "Project": 0, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 0, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 0, @@ -1913,6 +1945,10 @@ "Project": 0, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 0, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 0, @@ -2009,6 +2045,10 @@ "Project": 0, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 0, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 0, @@ -2129,6 +2169,10 @@ "Project": 1, "Uncalled": 1 }, + "PackageTypeCount": { + "Called": 1, + "Uncalled": 1 + }, "VulnCount": { "CallAnalysisCount": { "Called": 1, @@ -2237,6 +2281,10 @@ "Project": 1, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 1, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 1, @@ -2345,6 +2393,10 @@ "Project": 0, "Uncalled": 1 }, + "PackageTypeCount": { + "Called": 0, + "Uncalled": 1 + }, "VulnCount": { "CallAnalysisCount": { "Called": 0, @@ -2453,6 +2505,10 @@ "Project": 1, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 1, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 1, @@ -2561,6 +2617,10 @@ "Project": 1, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 1, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 1, @@ -2673,6 +2733,10 @@ "Project": 0, "Uncalled": 1 }, + "PackageTypeCount": { + "Called": 0, + "Uncalled": 1 + }, "VulnCount": { "CallAnalysisCount": { "Called": 0, @@ -2785,6 +2849,10 @@ "Project": 1, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 1, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 1, @@ -2935,6 +3003,10 @@ "Project": 2, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 2, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 2, @@ -3100,6 +3172,10 @@ "Project": 1, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 1, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 1, @@ -3277,6 +3353,10 @@ "Project": 2, "Uncalled": 0 }, + "PackageTypeCount": { + "Called": 2, + "Uncalled": 0 + }, "VulnCount": { "CallAnalysisCount": { "Called": 2, diff --git a/internal/output/output_result.go b/internal/output/output_result.go index 6e50da19f0..e56b8d6870 100644 --- a/internal/output/output_result.go +++ b/internal/output/output_result.go @@ -20,6 +20,7 @@ type Result struct { IsContainerScanning bool AllLayers []LayerInfo VulnTypeCount VulnTypeCount + PackageTypeCount CallAnalysisCount VulnCount VulnCount } @@ -190,10 +191,12 @@ func buildResult(ecosystemMap map[string][]SourceResult, resultCount VulnCount) isContainerScanning = true } vulnTypeCount := getVulnTypeCount(ecosystemResults) + packageTypeCount := getPackageTypeCount(ecosystemResults) return Result{ Ecosystems: ecosystemResults, VulnTypeCount: vulnTypeCount, + PackageTypeCount: packageTypeCount, VulnCount: resultCount, IsContainerScanning: isContainerScanning, AllLayers: layers, @@ -512,11 +515,11 @@ func getVulnTypeCount(result []EcosystemResult) VulnTypeCount { for _, ecosystem := range result { for _, source := range ecosystem.Sources { if ecosystem.IsOS { - vulnCount.OS += source.PackageTypeCount.Called + vulnCount.OS += source.VulnCount.CallAnalysisCount.Called } else { - vulnCount.Project += source.PackageTypeCount.Called + vulnCount.Project += source.VulnCount.CallAnalysisCount.Called } - vulnCount.Uncalled += source.PackageTypeCount.Uncalled + vulnCount.Uncalled += source.VulnCount.CallAnalysisCount.Uncalled } } @@ -525,6 +528,19 @@ func getVulnTypeCount(result []EcosystemResult) VulnTypeCount { return vulnCount } +func getPackageTypeCount(result []EcosystemResult) CallAnalysisCount { + var packageCount CallAnalysisCount + + for _, ecosystem := range result { + for _, source := range ecosystem.Sources { + packageCount.Called += source.PackageTypeCount.Called + packageCount.Uncalled += source.PackageTypeCount.Uncalled + } + } + + return packageCount +} + // calculateCount calculates the vulnerability counts based on the provided // lists of called and uncalled vulnerabilities. func calculateCount(calledVulnList, uncalledVulnList []VulnResult) VulnCount { diff --git a/internal/output/table.go b/internal/output/table.go index 39734e960a..663bc94844 100644 --- a/internal/output/table.go +++ b/internal/output/table.go @@ -28,11 +28,17 @@ func PrintTableResults(vulnResult *models.VulnerabilityResults, outputWriter io. text.DisableColors() } + outputResult := BuildResults(vulnResult) + // Render the vulnerabilities. - outputTable := newTable(outputWriter, terminalWidth) - outputTable = tableBuilder(outputTable, vulnResult) - if outputTable.Length() != 0 { - outputTable.Render() + if outputResult.IsContainerScanning { + printContainerScanningResult(outputResult, outputWriter, terminalWidth) + } else { + outputTable := newTable(outputWriter, terminalWidth) + outputTable = tableBuilder(outputTable, vulnResult) + if outputTable.Length() != 0 { + outputTable.Render() + } } // Render the licenses if any. @@ -84,6 +90,55 @@ func tableBuilder(outputTable table.Writer, vulnResult *models.VulnerabilityResu return outputTable } +func printContainerScanningResult(result Result, outputWriter io.Writer, terminalWidth int) { + summary := fmt.Sprintf( + "Total %[1]d packages affected by %[2]d vulnerabilities (%[3]d Critical, %[4]d High, %[5]d Medium, %[6]d Low, %[7]d Unknown) from %[8]d ecosystems, %[9]d have fixes available", + result.PackageTypeCount.Called, + result.VulnTypeCount.All, + result.VulnCount.SeverityCount.Critical, + result.VulnCount.SeverityCount.High, + result.VulnCount.SeverityCount.Medium, + result.VulnCount.SeverityCount.Low, + result.VulnCount.SeverityCount.Unknown, + len(result.Ecosystems), + result.VulnCount.FixableCount.Fixed, + ) + fmt.Fprintln(outputWriter, summary) + + for _, ecosystem := range result.Ecosystems { + fmt.Fprintln(outputWriter, ecosystem.Name) + + for _, source := range ecosystem.Sources { + outputTable := newTable(outputWriter, terminalWidth) + outputTable.SetTitle("Source:" + source.Name) + outputTable.AppendHeader(table.Row{"Package", "Installed Version", "Fix available", "Vuln count"}) + for _, pkg := range source.Packages { + outputRow := table.Row{} + totalCount := pkg.VulnCount.CallAnalysisCount.Called + var fixAvailable string + if pkg.FixedVersion == UnfixedDescription { + fixAvailable = UnfixedDescription + } else { + if pkg.VulnCount.FixableCount.UnFixed > 0 { + fixAvailable = "Partial fixes Available" + } else { + fixAvailable = "Fix Available" + } + } + outputRow = append(outputRow, pkg.Name, pkg.InstalledVersion, fixAvailable, totalCount) + outputTable.AppendRow(outputRow) + } + outputTable.Render() + } + } + + const promptMessage = "For the most comprehensive scan results, we recommend using the HTML output: " + + "`osv-scanner --format html --output results.html`. \n" + + "You can also view the full vulnerability list in your terminal with: " + + "`osv-scanner --format vertical`" + fmt.Fprintln(outputWriter, promptMessage) +} + type tbInnerResponse struct { row table.Row shouldMerge bool