src/goVulncheck: report unaffecting vulnerabilities separately

Unaffecting vulnerabilities = affect the required modules but call
paths to the vulnerable symbols from the analyzed packages are not
found.

And, in the run summary section, report the count of affecting
vulnerabilities.

Change-Id: I9ddd0ffed4286e9e942a056ee299d438f8b5f21a
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/412317
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/media/vulncheckView.js b/media/vulncheckView.js
index 140cbc7..7a61b84 100644
--- a/media/vulncheckView.js
+++ b/media/vulncheckView.js
@@ -12,6 +12,7 @@
 
 	const logContainer = /** @type {HTMLElement} */ (document.querySelector('.log'));
 	const vulnsContainer = /** @type {HTMLElement} */ (document.querySelector('.vulns'));
+	const unaffectingContainer = /** @type {HTMLElement} */ (document.querySelector('.unaffecting'));
 
 	vulnsContainer.addEventListener('click', (event) => {
 		let node = event && event.target;
@@ -37,7 +38,12 @@
 	}
 
 	function snapshotContent() {
-		return vulnsContainer.innerHTML;
+		const res = {
+			'log': logContainer.innerHTML,
+			'vulns': vulnsContainer.innerHTML,
+			'unaffecting': unaffectingContainer.innerHTML
+		};
+		return JSON.stringify(res);
 	}
 
 	/**
@@ -61,16 +67,19 @@
 			return durationMillisec ? `${startDate} (took ${durationMillisec} msec)` : `${startDate}`;
 		}
 
+		const vulns = json.Vuln || [];
+		const affecting = vulns.filter((v) => v.CallStackSummaries?.length);
+		const unaffecting = vulns.filter((v) => !v.CallStackSummaries?.length);
+		
 		runLog.innerHTML = `
 <tr><td>Dir:</td><td>${json.Dir || ''}</td></tr>
 <tr><td>Pattern:</td><td>${json.Pattern || ''}</td></tr>
-<tr><td>Analyzed at:</td><td>${timeinfo(json.Start, json.Duration)}</td></tr>`;
+<tr><td>Analyzed at:</td><td>${timeinfo(json.Start, json.Duration)}</td></tr>
+<tr><td>Found ${affecting?.length || 0} known vulnerabilities</td></tr>`;
 		logContainer.appendChild(runLog);
 
-		const vulns = json.Vuln || [];
 		vulnsContainer.innerHTML = '';
-
-		vulns.forEach((vuln) => {
+		affecting.forEach((vuln) => {
 			const element = document.createElement('div');
 			element.className = 'vuln';
 			vulnsContainer.appendChild(element);
@@ -92,7 +101,7 @@
 			details.className = 'vuln-details'
 			details.innerHTML = `
 			<tr><td>Package</td><td>${vuln.PkgPath}</td></tr>
-			<tr><td>Current Version</td><td>${moduleVersion(vuln.ModPath, vuln.CurrentVersion)}</td></tr>
+			<tr><td>Found in Version</td><td>${moduleVersion(vuln.ModPath, vuln.CurrentVersion)}</td></tr>
 			<tr><td>Fixed Version</td><td>${moduleVersion(vuln.ModPath, vuln.FixedVersion)}</td></tr>
 			<tr><td>Affecting</td><td>${vuln.AffectedPkgs?.join('<br>')}</td></tr>
 			`;
@@ -131,6 +140,20 @@
 			examples.appendChild(callstacksContainer);
 			element.appendChild(examples);
 		});
+
+		unaffectingContainer.innerText = '';
+		if (unaffecting.length > 0) {
+			unaffectingContainer.innerHTML = '<hr></hr><p>These vulnerabilities exist in required modules, but no vulnerable symbols are used.<br>No action is required. For more information, visit <a href="https://pkg.go.dev/vuln">https://pkg.go.dev/vuln</a></p>';
+
+			const details = document.createElement('table');
+			unaffecting.forEach((vuln) => {
+				const row = document.createElement('tr');
+				row.className = 'vuln-details'
+				row.innerHTML = `<tr><td>${vuln.ModPath}</td><td><a href="${vuln.URL}">${vuln.ID}</a></td></tr>`;
+				details.appendChild(row);
+			});
+			unaffectingContainer.appendChild(details);
+		}
 	}
 
 	// Message Passing between Extension and Webview
diff --git a/src/goVulncheck.ts b/src/goVulncheck.ts
index 5c53cd9..c070583 100644
--- a/src/goVulncheck.ts
+++ b/src/goVulncheck.ts
@@ -97,9 +97,9 @@
 				<title>Vulnerability Report - govulncheck</title>
 			</head>
 			<body>
-			    <div class="log"></div>
+				<div class="log"></div>
 				<div class="vulns"></div>
-				
+				<div class="unaffecting"></div>
 				<script nonce="${nonce}" src="${scriptUri}"></script>
 			</body>
 			</html>`;
@@ -201,7 +201,7 @@
 			const start = new Date();
 			const vuln = await vulncheck(goCtx, dir, pattern, this.channel);
 
-			if (vuln) {
+			if (vuln?.Vuln?.length) {
 				fillAffectedPkgs(vuln.Vuln);
 
 				// record run info.
@@ -222,7 +222,7 @@
 					VulncheckResultViewProvider.viewType,
 					viewColumn
 				);
-				this.channel.appendLine(`Vulncheck - result wrote in ${fname}`);
+				this.channel.appendLine(`Vulncheck - result written in ${fname}`);
 			} else {
 				this.channel.appendLine('Vulncheck - found no vulnerability');
 			}
diff --git a/test/gopls/vulncheck.test.ts b/test/gopls/vulncheck.test.ts
index 5afaa52..75fa8fc 100644
--- a/test/gopls/vulncheck.test.ts
+++ b/test/gopls/vulncheck.test.ts
@@ -49,11 +49,22 @@
 		const res = await watcher;
 
 		assert.deepStrictEqual(res.type, 'snapshot-result', `want snapshot-result, got ${JSON.stringify(res)}`);
-		assert(res.target && res.target.includes('GO-2021-0113'), res.target);
+		// res.target type is defined in vulncheckView.js.
+		const { log = '', vulns = '', unaffecting = '' } = JSON.parse(res.target ?? '{}');
+
 		assert(
-			res.target &&
-				res.target.includes('<td>Affecting</td><td>github.com/golang/vscode-go/test/testdata/vuln</td>'),
-			res.target
+			log.includes('Found 1 known vulnerabilities'),
+			`expected "1 known vulnerabilities", got ${JSON.stringify(res.target)}`
+		);
+		assert(
+			vulns.includes('GO-2021-0113') &&
+				vulns.includes('<td>Affecting</td><td>github.com/golang/vscode-go/test/testdata/vuln</td>'),
+			`expected "Affecting" section, got ${JSON.stringify(res.target)}`
+		);
+		// Unaffecting vulnerability's detail is omitted, but its ID is reported.
+		assert(
+			unaffecting.includes('GO-2021-0000') && unaffecting.includes('golang.org/x/text'),
+			`expected reports about unaffecting vulns, got ${JSON.stringify(res.target)}`
 		);
 	});
 
@@ -77,7 +88,8 @@
 		webviewPanel.webview.postMessage({ type: 'snapshot-request' });
 		const res = await watcher;
 		assert.deepStrictEqual(res.type, 'snapshot-result', `want snapshot-result, got ${JSON.stringify(res)}`);
-		assert(!res.target, res.target);
+		const { log = '', vulns = '', unaffecting = '' } = JSON.parse(res.target ?? '{}');
+		assert(!log && !vulns && !unaffecting, res.target);
 	});
 
 	// TODO: test corrupted/incomplete json file handling.
diff --git a/test/testdata/vuln/test.vulncheck.json b/test/testdata/vuln/test.vulncheck.json
index 3ebbb0e..653c554 100644
--- a/test/testdata/vuln/test.vulncheck.json
+++ b/test/testdata/vuln/test.vulncheck.json
@@ -36,6 +36,13 @@
       "CallStackSummaries": [
         "github.com/golang/vscode-go/test/testdata/vuln.main calls golang.org/x/text/language.Parse"
       ]
+    },
+    {
+      "ID": "GO-2021-0000",
+      "Details": "Bogus Report",
+      "Symbol": "Parse",
+      "ModPath": "golang.org/x/text",
+      "URL": "https://pkg.go.dev/vuln/GO-2021-0000"
     }
   ],
   "Start": "2022-05-16T13:43:54.437Z",