Skip to content

Commit 69e5534

Browse files
committed
src/goVulncheck: add VulncheckResultViewProvider
VulncheckResultViewProvider provides a custom text document editor to process `gopls vulncheck` JSON output and render it in a Webview. The webview presents the data in a similar way `govulncheck -html` presents the summary of the findings. But by implementing it inside the extension, we can make the extension handle commands and document links inside the editor. For example, in this CL, we embed a js script that listens 'click' event on all links in the HTML document, and delegates the extension part to handle link opening events. That way, if the resource is a file type, the extension can open it inside the editor. For communication between the Extension side and the WebView side, we use vscode's API to pass messages and follow the instruction in https://code.visualstudio.com/api/extension-guides/webview#scripts-and-message-passing The extension sends 'update' msg to the webview: that triggers update of DOM contents based on the input. The webview sends 'link' msg to the webview: that triggers handling of the link opening event. All file links in the callgraph entries carry the position information as a query parameter. The extension parses the uri, open the file with vscode.openWith command (that allows us to control which view column the editor should be placed, and which line in the file should get focus). In order to support testing, the extension and the webview exchange 'snapshot-request' and 'snapshot-result' messages. When we have UI testing framework established, these message may be able to be replaced with it. Change-Id: I9a343b972642a9197313d9d42af37b5a2d5ccefa Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/406300 TryBot-Result: kokoro <[email protected]> Run-TryBot: Hyang-Ah Hana Kim <[email protected]> Reviewed-by: Jamal Carvalho <[email protected]>
1 parent 84ac6ba commit 69e5534

File tree

7 files changed

+561
-0
lines changed

7 files changed

+561
-0
lines changed

media/reset.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2022 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
6+
html {
7+
box-sizing: border-box;
8+
font-size: 13px;
9+
}
10+
11+
*,
12+
*:before,
13+
*:after {
14+
box-sizing: inherit;
15+
}
16+
17+
body,
18+
h1,
19+
h2,
20+
h3,
21+
h4,
22+
h5,
23+
h6,
24+
p,
25+
ol,
26+
ul {
27+
margin: 0;
28+
padding: 0;
29+
font-weight: normal;
30+
}

media/vscode.css

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2022 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
6+
:root {
7+
--container-paddding: 20px;
8+
--input-padding-vertical: 6px;
9+
--input-padding-horizontal: 4px;
10+
--input-margin-vertical: 4px;
11+
--input-margin-horizontal: 0;
12+
}
13+
14+
body {
15+
padding: 0 var(--container-paddding);
16+
color: var(--vscode-foreground);
17+
font-size: var(--vscode-font-size);
18+
font-weight: var(--vscode-font-weight);
19+
font-family: var(--vscode-font-family);
20+
background-color: var(--vscode-editor-background);
21+
}
22+
23+
body>*,
24+
form>* {
25+
margin-block-start: var(--input-margin-vertical);
26+
margin-block-end: var(--input-margin-vertical);
27+
}
28+
29+
*:focus {
30+
outline-color: var(--vscode-focusBorder) !important;
31+
}
32+
33+
a {
34+
color: var(--vscode-textLink-foreground);
35+
}
36+
37+
a:hover,
38+
a:active {
39+
color: var(--vscode-textLink-activeForeground);
40+
}
41+
42+
code {
43+
font-size: var(--vscode-editor-font-size);
44+
font-family: var(--vscode-editor-font-family);
45+
}

media/vulncheckView.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2022 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
6+
.log {
7+
font-weight: lighter;
8+
}
9+
10+
.vuln {
11+
text-align: left;
12+
padding-bottom: 1em;
13+
}
14+
15+
.vuln-desc {
16+
padding-top: 0.5em;
17+
padding-bottom: 0.5em;
18+
}
19+
20+
.vuln-details {
21+
padding-bottom: 0.5em;
22+
}
23+
24+
details summary {
25+
cursor: pointer;
26+
position: relative;
27+
}
28+
29+
details summary>* {
30+
display: inline;
31+
position: relative;
32+
}
33+
34+
.stacks {
35+
padding: 1em;
36+
}
37+
38+
.stack {
39+
padding: 1em;
40+
font-size: var(--vscode-editor-font-size);
41+
font-family: var(--vscode-editor-font-family);
42+
}

media/vulncheckView.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2022 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
6+
// Script for VulncheckResultViewProvider's webview.
7+
8+
(function () {
9+
10+
// @ts-ignore
11+
const vscode = acquireVsCodeApi();
12+
13+
const logContainer = /** @type {HTMLElement} */ (document.querySelector('.log'));
14+
const vulnsContainer = /** @type {HTMLElement} */ (document.querySelector('.vulns'));
15+
16+
vulnsContainer.addEventListener('click', (event) => {
17+
let node = event && event.target;
18+
if (node?.tagName === 'A' && node.href) {
19+
// Ask vscode to handle link opening.
20+
vscode.postMessage({ type: 'open', target: node.href });
21+
event.preventDefault();
22+
event.stopPropagation();
23+
return;
24+
}
25+
});
26+
27+
const errorContainer = document.createElement('div');
28+
document.body.appendChild(errorContainer);
29+
errorContainer.className = 'error'
30+
errorContainer.style.display = 'none'
31+
32+
function moduleVersion(/** @type {string} */mod, /** @type {string|undefined} */ver) {
33+
if (ver) {
34+
return `<a href="https://pkg.go.dev/${mod}@${ver}">${mod}@${ver}</a>`;
35+
}
36+
return 'N/A'
37+
}
38+
39+
function snapshotContent() {
40+
return vulnsContainer.innerHTML;
41+
}
42+
43+
/**
44+
* Render the document in the webview.
45+
*/
46+
function updateContent(/** @type {string} */ text = '{}') {
47+
let json;
48+
try {
49+
json = JSON.parse(text);
50+
} catch {
51+
errorContainer.innerText = 'Error: Document is not valid json';
52+
errorContainer.style.display = '';
53+
return;
54+
}
55+
errorContainer.style.display = 'none';
56+
57+
logContainer.innerHTML = '';
58+
const runLog = document.createElement('table');
59+
const timeinfo = (startDate, durationMillisec) => {
60+
if (!startDate) { return '' }
61+
return durationMillisec ? `${startDate} (took ${durationMillisec} msec)` : `${startDate}`;
62+
}
63+
64+
runLog.innerHTML = `
65+
<tr><td>Dir:</td><td>${json.Dir || ''}</td></tr>
66+
<tr><td>Pattern:</td><td>${json.Pattern || ''}</td></tr>
67+
<tr><td>Analyzed at:</td><td>${timeinfo(json.Start, json.Duration)}</td></tr>`;
68+
logContainer.appendChild(runLog);
69+
70+
const vulns = json.Vuln || [];
71+
vulnsContainer.innerHTML = '';
72+
73+
vulns.forEach((vuln) => {
74+
const element = document.createElement('div');
75+
element.className = 'vuln';
76+
vulnsContainer.appendChild(element);
77+
78+
// TITLE - Vuln ID
79+
const title = document.createElement('h2');
80+
title.innerHTML = `<a href="${vuln.URL}">${vuln.ID}</a>`;
81+
title.className = 'vuln-title';
82+
element.appendChild(title);
83+
84+
// DESCRIPTION - short text (aliases)
85+
const desc = document.createElement('p');
86+
desc.innerHTML = Array.isArray(vuln.Aliases) && vuln.Aliases.length ? `${vuln.Details} (${vuln.Aliases.join(', ')})` : vuln.Details;
87+
desc.className = 'vuln-desc';
88+
element.appendChild(desc);
89+
90+
// DETAILS - dump of all details
91+
const details = document.createElement('table');
92+
details.className = 'vuln-details'
93+
details.innerHTML = `
94+
<tr><td>Package</td><td>${vuln.PkgPath}</td></tr>
95+
<tr><td>Current Version</td><td>${moduleVersion(vuln.ModPath, vuln.CurrentVersion)}</td></tr>
96+
<tr><td>Fixed Version</td><td>${moduleVersion(vuln.ModPath, vuln.FixedVersion)}</td></tr>
97+
`;
98+
element.appendChild(details);
99+
100+
/* TODO: Action for module version upgrade */
101+
/* TODO: Explain module dependency - why am I depending on this vulnerable version? */
102+
103+
// EXEMPLARS - call stacks (initially hidden)
104+
const examples = document.createElement('details');
105+
examples.innerHTML = `<summary>${vuln.CallStackSummaries?.length || 0}+ findings</summary>`;
106+
107+
// Call stacks
108+
const callstacksContainer = document.createElement('p');
109+
callstacksContainer.className = 'stacks';
110+
vuln.CallStackSummaries?.forEach((summary, idx) => {
111+
const callstack = document.createElement('details');
112+
const s = document.createElement('summary');
113+
s.innerText = summary;
114+
callstack.appendChild(s);
115+
116+
const stack = document.createElement('div');
117+
stack.className = 'stack';
118+
const cs = vuln.CallStacks[idx];
119+
cs.forEach((c) => {
120+
const p = document.createElement('p');
121+
const pos = c.URI ? `${c.URI}?${c.Pos.line || 0}` : '';
122+
p.innerHTML = pos ? `<a href="${pos}">${c.Name}</a>` : c.Name;
123+
stack.appendChild(p);
124+
});
125+
callstack.appendChild(stack);
126+
127+
callstacksContainer.appendChild(callstack);
128+
})
129+
130+
examples.appendChild(callstacksContainer);
131+
element.appendChild(examples);
132+
});
133+
}
134+
135+
// Message Passing between Extension and Webview
136+
//
137+
// Extension sends 'update' to Webview to trigger rerendering.
138+
// Webview sends 'link' to Extension to forward all link
139+
// click events so the extension can handle the event.
140+
//
141+
// Extension sends 'snapshot-request' to trigger dumping
142+
// of the current DOM in the 'vulns' container.
143+
// Webview sends 'snapshot-result' to the extension
144+
// as the response to snapshot-request.
145+
146+
// Handle messages sent from the extension to the webview
147+
window.addEventListener('message', event => {
148+
const message = event.data; // The json data that the extension sent
149+
switch (message.type) {
150+
case 'update':
151+
const text = message.text;
152+
153+
updateContent(text);
154+
// Then persist state information.
155+
// This state is returned in the call to `vscode.getState` below when a webview is reloaded.
156+
vscode.setState({ text });
157+
return;
158+
// Message for testing. Returns a current DOM in a serialized format.
159+
case 'snapshot-request':
160+
const result = snapshotContent();
161+
vscode.postMessage({ type: 'snapshot-result', target: result });
162+
return;
163+
}
164+
});
165+
166+
// Webviews are normally torn down when not visible and re-created when they become visible again.
167+
// State lets us save information across these re-loads
168+
const state = vscode.getState();
169+
if (state) {
170+
updateContent(state.text);
171+
};
172+
// TODO: Handle 'details' expansion info and store the state using
173+
// vscode.setState or retainContextWhenHidden. Currently, we are storing only
174+
// the document text. (see windowEventHandler)
175+
}());

0 commit comments

Comments
 (0)