media/vulncheckView: organize unaffecting vulns by vuln ids

This matches the latest govulncheck's behavior.
With this change, we can also present the details about
each unaffecting vulnerability and fixes.

gopls vulncheck (v0.9.5) yet doesn't provide package names and
currently used version info for unaffecting vulnerabilities.
Once that is fixed in gopls, we can make the unaffecting
vulnerability section more like the main, known vulnerability
section.

Change-Id: Ifb9bc7272346c4c78606feeea165f485d60b9cf6
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/429236
Reviewed-by: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/media/vulncheckView.css b/media/vulncheckView.css
index 289c8d4..6cef736 100644
--- a/media/vulncheckView.css
+++ b/media/vulncheckView.css
@@ -3,8 +3,10 @@
  * Licensed under the MIT License. See LICENSE in the project root for license information.
  *--------------------------------------------------------*/
 
-.log {
+.log,
+.info {
 	font-weight: lighter;
+	padding-bottom: 1em;
 }
 
 .vuln {
diff --git a/media/vulncheckView.js b/media/vulncheckView.js
index 118942f..d95e7d5 100644
--- a/media/vulncheckView.js
+++ b/media/vulncheckView.js
@@ -36,14 +36,32 @@
 	errorContainer.className = 'error'
 	errorContainer.style.display = 'none'
 
-	function moduleVersion(/** @type {string} */mod, /** @type {string|undefined} */ver) {
-		if (ver) {
-			return `<a href="https://pkg.go.dev/${mod}@${ver}">${mod}@${ver}</a>`;
+	function packageVersion(/** @type {string} */mod, /** @type {string} */pkg, /** @type {string|undefined} */ver) {
+		if (!ver) {
+			return 'N/A';
 		}
-		return 'N/A'
+
+		if (mod === 'stdlib' && ver.startsWith('v')) {
+			ver = `go${ver.slice(1)}`;
+		}
+		return `<a href="https://pkg.go.dev/${pkg}@${ver}">${pkg}@${ver}</a>`;
+	}
+
+	function modVersion(/** @type {string} */mod, /** @type {string|undefined} */ver) {
+		if (!ver) {
+			return 'N/A';
+		}
+
+		if (mod === 'stdlib' && ver.startsWith('v')) {
+			ver = `go${ver.slice(1)}`;
+		}
+		return `<a href="https://pkg.go.dev/${mod}@${ver}">${mod}@${ver}</a>`;
 	}
 
 	function offerUpgrade(/** @type {string} */dir, /** @type {string} */mod, /** @type {string|undefined} */ver) {
+		if (mod === 'stdlib') {
+			return '';
+		}
 		if (dir && mod && ver) {
 			return ` [<span class="vuln-fix" data-target="${mod}@${ver}" data-dir="${dir}">go get</span> | <span class="vuln-fix" data-target="${mod}@latest" data-dir="${dir}">go get latest</span>]`
 		}
@@ -83,12 +101,16 @@
 		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>Found ${affecting?.length || 0} known vulnerabilities</td></tr>`;
+<tr><td>Found</td><td>${affecting?.length || 0} known vulnerabilities</td></tr>`;
+		if (unaffecting?.length > 0) {
+			runLog.innerHTML += `<tr><td>Found</td><td>${unaffecting.length} informational vulnerabilities</td></tr>`
+		}
+
 		logContainer.appendChild(runLog);
 
 		vulnsContainer.innerHTML = '';
@@ -114,8 +136,8 @@
 			details.className = 'vuln-details'
 			details.innerHTML = `
 			<tr><td>Package</td><td>${vuln.PkgPath}</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)} ${offerUpgrade(json.Dir, vuln.ModPath, vuln.FixedVersion)}</td></tr>
+			<tr><td>Found in Version</td><td>${packageVersion(vuln.ModPath, vuln.PkgPath, vuln.CurrentVersion)}</td></tr>
+			<tr><td>Fixed Version</td><td>${packageVersion(vuln.ModPath, vuln.PkgPath, vuln.FixedVersion)} ${offerUpgrade(json.Dir, vuln.ModPath, vuln.FixedVersion)}</td></tr>
 			<tr><td>Affecting</td><td>${vuln.AffectedPkgs?.join('<br>')}</td></tr>
 			`;
 			element.appendChild(details);
@@ -156,16 +178,50 @@
 
 		unaffectingContainer.innerText = '';
 		if (unaffecting.length > 0) {
-			unaffectingContainer.innerHTML = '<hr></hr><p>The vulnerabilities below are in packages that you import, but your code does not appear to call any vulnerable functions. You may not need to take any action. See <a href="https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck">https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck</a> for details.';
+			const notice = document.createElement('div');
+			notice.className = 'info';
+			notice.innerHTML = `
+<hr></hr>The vulnerabilities below are in packages that you import, 
+but your code does not appear to call any vulnerable functions. 
+You may not need to take any action. See 
+<a href="https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck">
+https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck</a>
+for details.
+`;
 
-			const details = document.createElement('table');
+			unaffectingContainer.appendChild(notice);
+
 			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);
+				const element = document.createElement('div');
+				element.className = 'vuln';
+				unaffectingContainer.appendChild(element);
+
+				// TITLE - Vuln ID
+				const title = document.createElement('h2');
+				title.innerHTML = `<a href="${vuln.URL}">${vuln.ID}</a>`;
+				title.className = 'vuln-title';
+				element.appendChild(title);
+
+				// DESCRIPTION - short text (aliases)
+				const desc = document.createElement('p');
+				desc.innerHTML = Array.isArray(vuln.Aliases) && vuln.Aliases.length ? `${vuln.Details} (${vuln.Aliases.join(', ')})` : vuln.Details;
+				desc.className = 'vuln-desc';
+				element.appendChild(desc);
+
+				// DETAILS - dump of all details
+				// TODO(hyangah):
+				//   - include the current version & package name when gopls provides them.
+				//   - offer upgrade like affect vulnerabilities. We will need to install another event listener
+				//     on unaffectingContainer. See vulnsContainer.addEventListener.
+				const details = document.createElement('table');
+				details.className = 'vuln-details'
+				if (vuln.FixedVersion) {
+					details.innerHTML = `<tr><td>Fixed Version</td><td>${modVersion(vuln.ModPath, vuln.FixedVersion)}</td></tr>`;
+				} else {
+					details.innerHTML = `<tr><td>Fixed Version</td><td>unavailable for ${vuln.ModPath}</td></tr>`;
+				}
+				element.appendChild(details);
 			});
-			unaffectingContainer.appendChild(details);
 		}
 	}
 
diff --git a/test/gopls/vulncheck.test.ts b/test/gopls/vulncheck.test.ts
index 2706b66..46699e5 100644
--- a/test/gopls/vulncheck.test.ts
+++ b/test/gopls/vulncheck.test.ts
@@ -59,7 +59,7 @@
 			const { log = '', vulns = '', unaffecting = '' } = JSON.parse(res.target ?? '{}');
 
 			assert(
-				log.includes('Found 1 known vulnerabilities'),
+				log.includes('1 known vulnerabilities'),
 				`expected "1 known vulnerabilities", got ${JSON.stringify(res.target)}`
 			);
 			assert(
@@ -67,7 +67,7 @@
 					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.
+			// Unaffecting vulnerability's 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)}`