Repository: sonatype-nexus-community/nancy
Pull request #248 (2021-09-18 02:43:34)

show number of ignored vulnerabilities in summary table
This pull request makes the following changes:
* Added an exclusion count to auditlog and auditlogtextformatter
* Now in summary table, there is a number of ignored deps highlighted in yellow, kinda like a warning

I'm unsure if y'all would rather it not be in the table though. I also think I could probably just list out the dependencies that are ignored but I am just now re-learning go so baby steps :)

I REALLY miss you all. <3

It relates to the following issue #s:
* Fixes  #232

cc @bhamail / @DarthHater


sonatypecla[bot]: Thanks for the contribution!  Unfortunately we can't verify if the committer(s), Michel Kazi <michel.kazi@viacomcontractor.com>, signed the CLA because they have not associated their commits with their GitHub user.  Please follow [these instructions](https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/#commits-are-not-linked-to-any-user) to associate your commits with your GitHub user.  Then [sign the Sonatype Contributor License Agreement](https://sonatypecla.herokuapp.com/status/sonatype-nexus-community/nancy/pull/248) and this Pull Request will be revalidated.
MichelKazi: Feels like a brute force leetcode solution to nest a for loop and check for vulnerabilities here

maybe I can add an Exclusions field here https://github.com/sonatype-nexus-community/go-sona-types/blob/main/ossindex/types/types.go and just get the length of that or something
MichelKazi: @svilgelm 
![image](https://user-images.githubusercontent.com/34050448/133872417-8dcc039c-a9ce-407b-99f0-8cb43eadcf77.png)

Let me know what you think
SVilgelm: Looks good 👍 
zendern: PR changes look good to me. I think we could use a test to validate it does in fact get printed out.

See here : 
https://github.com/sonatype-nexus-community/nancy/blob/main/internal/audit/auditlogtextformatter_test.go

And if you are up for it we might as well add it to the csv formatter too. 
https://github.com/sonatype-nexus-community/nancy/blob/main/internal/audit/csvformatter.go

json output it already happens for free as we just serialize the object that gets passed in. 

Patch:
diff --git a/internal/audit/auditlog.go b/internal/audit/auditlog.go
index 2865018..5b5a02d 100644
--- a/internal/audit/auditlog.go
+++ b/internal/audit/auditlog.go
@@ -28,6 +28,7 @@ import (
 // results.
 func LogResults(formatter log.Formatter, packageCount int, coordinates []types.Coordinate, invalidCoordinates []types.Coordinate, exclusions []string) int {
 	vulnerableCount := 0
+	exclusionCount := 0
 
 	for _, c := range coordinates {
 		c.ExcludeVulnerabilities(exclusions)
@@ -35,9 +36,16 @@ func LogResults(formatter log.Formatter, packageCount int, coordinates []types.C
 
 	var auditedCoordinates []types.Coordinate
 	var vulnerableCoordinates []types.Coordinate
+	var excludedVulnerabilities []types.Vulnerability
 
 	for i := 0; i < len(coordinates); i++ {
 		coordinate := coordinates[i]
+		for _, v := range coordinate.Vulnerabilities {
+			if v.Excluded {
+				exclusionCount++
+				excludedVulnerabilities = append(excludedVulnerabilities, v)
+			}
+		}
 		if coordinate.IsVulnerable() {
 			vulnerableCount++
 			vulnerableCoordinates = append(vulnerableCoordinates, coordinate)
@@ -54,14 +62,17 @@ func LogResults(formatter log.Formatter, packageCount int, coordinates []types.C
 	if vulnerableCoordinates == nil {
 		vulnerableCoordinates = make([]types.Coordinate, 0)
 	}
+
 	log.SetFormatter(formatter)
 	log.SetOutput(os.Stdout)
 	log.WithFields(log.Fields{
 		"exclusions":     exclusions,
 		"num_audited":    packageCount,
 		"num_vulnerable": vulnerableCount,
+		"num_exclusions": exclusionCount,
 		"audited":        auditedCoordinates,
 		"vulnerable":     vulnerableCoordinates,
+		"excluded":       excludedVulnerabilities,
 		"invalid":        invalidCoordinates,
 		"version":        buildversion.BuildVersion,
 	}).Info("")
diff --git a/internal/audit/auditlogtextformatter.go b/internal/audit/auditlogtextformatter.go
index d513fca..c8f2667 100644
--- a/internal/audit/auditlogtextformatter.go
+++ b/internal/audit/auditlogtextformatter.go
@@ -153,14 +153,17 @@ func groupAndPrint(vulnerable []types.Coordinate, nonVulnerable []types.Coordina
 func (f AuditLogTextFormatter) Format(entry *Entry) ([]byte, error) {
 	auditedEntries := entry.Data["audited"]
 	invalidEntries := entry.Data["invalid"]
+	excludedEntries := entry.Data["excluded"]
 	packageCount := entry.Data["num_audited"]
 	numVulnerable := entry.Data["num_vulnerable"]
+	numExcluded := entry.Data["num_exclusions"]
 	buildVersion := entry.Data["version"]
-	if auditedEntries != nil && invalidEntries != nil && packageCount != nil && numVulnerable != nil && buildVersion != nil {
+	if auditedEntries != nil && invalidEntries != nil && excludedEntries != nil && packageCount != nil && numVulnerable != nil && numExcluded != nil && buildVersion != nil {
 		auditedEntries := entry.Data["audited"].([]types.Coordinate)
 		invalidEntries := entry.Data["invalid"].([]types.Coordinate)
 		packageCount := entry.Data["num_audited"].(int)
 		numVulnerable := entry.Data["num_vulnerable"].(int)
+		numExcluded := entry.Data["num_exclusions"].(int)
 
 		var sb strings.Builder
 
@@ -179,6 +182,10 @@ func (f AuditLogTextFormatter) Format(entry *Entry) ([]byte, error) {
 		t.AppendRow([]interface{}{"Audited Dependencies", strconv.Itoa(packageCount)})
 		t.AppendSeparator()
 		t.AppendRow([]interface{}{"Vulnerable Dependencies", au.Bold(au.Red(strconv.Itoa(numVulnerable)))})
+    if numExcluded > 0 {
+      t.AppendSeparator()
+      t.AppendRow([]interface{}{"Ignored Vulnerabilities", au.Bold(au.Yellow(strconv.Itoa(numExcluded)))})
+    }
 		sb.WriteString(t.Render())
 		sb.WriteString("\n")
 
diff --git a/internal/audit/csvformatter.go b/internal/audit/csvformatter.go
index 53fb797..34f3f53 100644
--- a/internal/audit/csvformatter.go
+++ b/internal/audit/csvformatter.go
@@ -41,19 +41,21 @@ func (f CsvFormatter) Format(entry *Entry) ([]byte, error) {
 	invalidEntries := entry.Data["invalid"]
 	packageCount := entry.Data["num_audited"]
 	numVulnerable := entry.Data["num_vulnerable"]
+	excludedCount := entry.Data["num_exclusions"]
 	buildVersion := entry.Data["version"]
 
-	if auditedEntries != nil && invalidEntries != nil && packageCount != nil && numVulnerable != nil && buildVersion != nil {
+	if auditedEntries != nil && invalidEntries != nil && packageCount != nil && numVulnerable != nil && excludedCount != nil && buildVersion != nil {
 		auditedEntries := entry.Data["audited"].([]types.Coordinate)
 		invalidEntries := entry.Data["invalid"].([]types.Coordinate)
 		packageCount := entry.Data["num_audited"].(int)
 		numVulnerable := entry.Data["num_vulnerable"].(int)
+		excludedCount := entry.Data["num_exclusions"].(int)
 		buildVersion := entry.Data["version"].(string)
 
-		var summaryHeader = []string{"Audited Count", "Vulnerable Count", "Build Version"}
+		var summaryHeader = []string{"Audited Count", "Vulnerable Count", "Ignored Vulnerabilities", "Build Version"}
 		var invalidHeader = []string{"Count", "Package", "Reason"}
 		var auditedHeader = []string{"Count", "Package", "Is Vulnerable", "Num Vulnerabilities", "Vulnerabilities"}
-		var summaryRow = []string{strconv.Itoa(packageCount), strconv.Itoa(numVulnerable), buildVersion}
+		var summaryRow = []string{strconv.Itoa(packageCount), strconv.Itoa(numVulnerable), strconv.Itoa(excludedCount), buildVersion}
 
 		var buf bytes.Buffer
 		w := csv.NewWriter(&buf)


Test patch:
diff --git a/internal/audit/auditlogtextformatter_test.go b/internal/audit/auditlogtextformatter_test.go
index e8fc670..b23a39c 100644
--- a/internal/audit/auditlogtextformatter_test.go
+++ b/internal/audit/auditlogtextformatter_test.go
@@ -47,6 +47,8 @@ func verifyFormatterSummaryLoudness(t *testing.T, quiet bool) {
 	data := map[string]interface{}{
 		"audited":        []types.Coordinate{},
 		"invalid":        []types.Coordinate{},
+		"excluded":       []types.Vulnerability{},
+		"num_exclusions": 0,
 		"num_audited":    0,
 		"num_vulnerable": 0,
 		"version":        0,
@@ -78,6 +80,8 @@ func TestFormatterLogInvalidSemVerWarning(t *testing.T) {
 				Coordinates: "MyInvalidCoords",
 			},
 		},
+		"excluded":       []types.Vulnerability{},
+		"num_exclusions": 0,
 		"num_audited":    0,
 		"num_vulnerable": 0,
 		"version":        0,
@@ -114,3 +118,25 @@ func TestScoreAssessment(t *testing.T) {
 	assert.Equal(t, "Medium", scoreAssessment(decimal.New(4, 0)))
 	assert.Equal(t, "Low", scoreAssessment(decimal.New(0, 0)))
 }
+
+func TestExcludedVulnsInSummary(t *testing.T) {
+	data := map[string]interface{}{
+		"audited":        []types.Coordinate{},
+		"invalid":        []types.Coordinate{},
+		"excluded":       []types.Vulnerability{},
+		"num_audited":    0,
+		"num_exclusions": 1,
+		"num_vulnerable": 0,
+		"version":        0,
+	}
+	entry := Entry{Data: data}
+
+	formatter := AuditLogTextFormatter{}
+	logMessage, e := formatter.Format(&entry)
+	assert.Nil(t, e)
+
+	expectedSummary := "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n┃ Summary                     ┃\n┣━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━┫\n┃ Audited Dependencies    ┃ 0 ┃\n┣━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━┫\n┃ Vulnerable Dependencies ┃ \x1b[1;31m0\x1b[0m ┃\n┣━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━┫\n┃ Ignored Vulnerabilities ┃ \x1b[1;33m1\x1b[0m ┃\n┗━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━┛\n"
+	expectedSummary = "\n\n0 Non Vulnerable Packages\n\n" + expectedSummary
+	assert.Equal(t, expectedSummary, string(logMessage))
+	// assert that ignored vulns show up
+}
diff --git a/internal/audit/csvformatter_test.go b/internal/audit/csvformatter_test.go
index aed63e3..e6b49bc 100644
--- a/internal/audit/csvformatter_test.go
+++ b/internal/audit/csvformatter_test.go
@@ -36,6 +36,7 @@ func TestCsvOutputWhenQuiet(t *testing.T) {
 		},
 		"num_audited":    2,
 		"num_vulnerable": 1,
+		"num_exclusions": 0,
 		"version":        "development",
 	}
 	entry := Entry{Data: data}
@@ -44,8 +45,8 @@ func TestCsvOutputWhenQuiet(t *testing.T) {
 	logMessage, e := formatter.Format(&entry)
 	assert.Nil(t, e)
 	expectedCsv := `Summary
-Audited Count,Vulnerable Count,Build Version
-2,1,development
+Audited Count,Vulnerable Count,Ignored Vulnerabilities,Build Version
+2,1,0,development
 
 Audited Package(s)
 Count,Package,Is Vulnerable,Num Vulnerabilities,Vulnerabilities
@@ -65,6 +66,7 @@ func TestCsvOutput(t *testing.T) {
 		},
 		"num_audited":    2,
 		"num_vulnerable": 1,
+		"num_exclusions": 0,
 		"version":        "development",
 	}
 	entry := Entry{Data: data}
@@ -73,8 +75,8 @@ func TestCsvOutput(t *testing.T) {
 	logMessage, e := formatter.Format(&entry)
 	assert.Nil(t, e)
 	expectedCsv := `Summary
-Audited Count,Vulnerable Count,Build Version
-2,1,development
+Audited Count,Vulnerable Count,Ignored Vulnerabilities,Build Version
+2,1,0,development
 
 Invalid Package(s)
 Count,Package,Reason
@@ -115,9 +117,10 @@ func TestCsvFormatter_FormatNoError(t *testing.T) {
 		},
 		"num_audited":    0,
 		"num_vulnerable": 0,
+		"num_exclusions": 0,
 		"version":        "theBuildVersion",
 	}
 	buf, err := formatter.Format(&Entry{Data: data})
 	assert.NoError(t, err)
-	assert.Equal(t, "Summary\nAudited Count,Vulnerable Count,Build Version\n0,0,theBuildVersion\n", string(buf))
+	assert.Equal(t, "Summary\nAudited Count,Vulnerable Count,Ignored Vulnerabilities,Build Version\n0,0,0,theBuildVersion\n", string(buf))
 }