Skip to content

Commit 150c57f

Browse files
committed
src/goVulncheck: show the vulncheck result in vulncheck.view
After running vulncheck, open the custom editor (implmeneted by VulncheckResultViewProvider) to show the result. This CL also computes 'AffectedPkgs' field of each vulnerability. This computation should be done inside gopls, rather than in vscode. But while waiting for another gopls release, let's handle here. (For forwards-compatibility, we skip this if AffectedPkgs field is already populated). Change-Id: Ie29050a08955ace05c7c8f478b889554168de09b Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/406302 Reviewed-by: Suzy Mueller <[email protected]> Run-TryBot: Hyang-Ah Hana Kim <[email protected]> Reviewed-by: Jamal Carvalho <[email protected]> TryBot-Result: kokoro <[email protected]>
1 parent 0d481b8 commit 150c57f

File tree

4 files changed

+105
-51
lines changed

4 files changed

+105
-51
lines changed

media/vulncheckView.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
<tr><td>Package</td><td>${vuln.PkgPath}</td></tr>
9595
<tr><td>Current Version</td><td>${moduleVersion(vuln.ModPath, vuln.CurrentVersion)}</td></tr>
9696
<tr><td>Fixed Version</td><td>${moduleVersion(vuln.ModPath, vuln.FixedVersion)}</td></tr>
97+
<tr><td>Affecting</td><td>${vuln.AffectedPkgs?.join('<br>')}</td></tr>
9798
`;
9899
element.appendChild(details);
99100

src/goVulncheck.ts

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------*/
55

66
import path from 'path';
7+
import fs from 'fs';
78
import * as vscode from 'vscode';
89
import { GoExtensionContext } from './context';
910
import { getBinPath } from './util';
@@ -12,6 +13,7 @@ import { toolExecutionEnvironment } from './goEnv';
1213
import { killProcessTree } from './utils/processUtils';
1314
import * as readline from 'readline';
1415
import { URI } from 'vscode-uri';
16+
import { promisify } from 'util';
1517

1618
export class VulncheckResultViewProvider implements vscode.CustomTextEditorProvider {
1719
public static readonly viewType = 'vulncheck.view';
@@ -195,26 +197,39 @@ export class VulncheckProvider {
195197
this.channel.clear();
196198
this.channel.appendLine(`cd ${dir}; gopls vulncheck ${pattern}`);
197199

198-
let result = '';
199200
try {
200201
const start = new Date();
201202
const vuln = await vulncheck(goCtx, dir, pattern, this.channel);
202203

203204
if (vuln) {
205+
fillAffectedPkgs(vuln.Vuln);
206+
204207
// record run info.
205208
vuln.Start = start;
206209
vuln.Duration = Date.now() - start.getTime();
207210
vuln.Dir = dir;
208211
vuln.Pattern = pattern;
209-
result = vuln.Vuln
210-
? vuln.Vuln.map(renderVuln).join('----------------------\n')
211-
: 'No known vulnerability found.';
212+
213+
// write to file and visualize it!
214+
const fname = path.join(dir, `vulncheck-${Date.now()}.vulncheck.json`);
215+
const writeFile = promisify(fs.writeFile);
216+
await writeFile(fname, JSON.stringify(vuln));
217+
const uri = URI.file(fname);
218+
const viewColumn = vscode.ViewColumn.Beside;
219+
vscode.commands.executeCommand(
220+
'vscode.openWith',
221+
uri,
222+
VulncheckResultViewProvider.viewType,
223+
viewColumn
224+
);
225+
this.channel.appendLine(`Vulncheck - result wrote in ${fname}`);
226+
} else {
227+
this.channel.appendLine('Vulncheck - found no vulnerability');
212228
}
213229
} catch (e) {
214230
vscode.window.showErrorMessage(`error running vulncheck: ${e}`);
215231
this.channel.appendLine(`Vulncheck failed: ${e}`);
216232
}
217-
this.channel.appendLine(result);
218233
this.channel.show();
219234
}
220235

@@ -300,48 +315,6 @@ export async function vulncheck(
300315
return await task;
301316
}
302317

303-
const renderVuln = (v: Vuln) => `
304-
ID: ${v.ID}
305-
Aliases: ${v.Aliases?.join(', ') ?? 'None'}
306-
Symbol: ${v.Symbol}
307-
Pkg Path: ${v.PkgPath}
308-
Mod Path: ${v.ModPath}
309-
URL: ${v.URL}
310-
Current Version: ${v.CurrentVersion}
311-
Fixed Version: ${v.FixedVersion}
312-
313-
${v.Details}
314-
${renderStack(v)}`;
315-
316-
const renderStack = (v: Vuln) => {
317-
const content = [];
318-
for (const stack of v.CallStacks ?? []) {
319-
for (const [, line] of stack.entries()) {
320-
content.push(`\t${line.Name}`);
321-
const loc = renderUri(line);
322-
if (loc) {
323-
content.push(`\t\t${loc}`);
324-
}
325-
}
326-
content.push('');
327-
}
328-
return content.join('\n');
329-
};
330-
331-
const renderUri = (stack: CallStack) => {
332-
if (!stack.URI) {
333-
// generated file or dummy location may not have a file name.
334-
return '';
335-
}
336-
const parsed = vscode.Uri.parse(stack.URI);
337-
const line = stack.Pos.line + 1; // Position uses 0-based line number.
338-
const folder = vscode.workspace.getWorkspaceFolder(parsed);
339-
if (folder) {
340-
return `${parsed.path}:${line}:${stack.Pos.character}`;
341-
}
342-
return `${stack.URI}#${line}:${stack.Pos.character}`;
343-
};
344-
345318
interface VulncheckReport {
346319
// Vulns populated by gopls vulncheck run.
347320
Vuln?: Vuln[];
@@ -366,6 +339,10 @@ interface Vuln {
366339
FixedVersion: string;
367340
CallStacks?: CallStack[][];
368341
CallStacksSummary?: string[];
342+
343+
// Derived from call stacks.
344+
// TODO(hyangah): add to gopls vulncheck.
345+
AffectedPkgs?: string[];
369346
}
370347

371348
interface CallStack {
@@ -393,3 +370,33 @@ function safeURIParse(s: string): URI | undefined {
393370
return undefined;
394371
}
395372
}
373+
374+
// Computes the AffectedPkgs attribute if it's not present.
375+
// Exported for testing.
376+
// TODO(hyangah): move this logic to gopls vulncheck or govulncheck.
377+
export function fillAffectedPkgs(vulns: Vuln[] | undefined): Vuln[] {
378+
if (!vulns) return [];
379+
380+
const re = new RegExp(/^(\S+)\/([^/\s]+)$/);
381+
vulns.forEach((vuln) => {
382+
// If it's already set by gopls vulncheck, great!
383+
if (vuln.AffectedPkgs) return;
384+
385+
const affected = new Set<string>();
386+
vuln.CallStacks?.forEach((cs) => {
387+
if (!cs || cs.length === 0) {
388+
return;
389+
}
390+
const name = cs[0].Name || '';
391+
const m = name.match(re);
392+
if (!m) {
393+
name && affected.add(name);
394+
} else {
395+
const pkg = m[2] && m[2].split('.')[0];
396+
affected.add(`${m[1]}/${pkg}`);
397+
}
398+
});
399+
vuln.AffectedPkgs = Array.from(affected);
400+
});
401+
return vulns;
402+
}

test/gopls/vulncheck.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,14 @@ suite('vulncheck result viewer tests', () => {
5050

5151
assert.deepStrictEqual(res.type, 'snapshot-result', `want snapshot-result, got ${JSON.stringify(res)}`);
5252
assert(res.target && res.target.includes('GO-2021-0113'), res.target);
53+
assert(
54+
res.target &&
55+
res.target.includes('<td>Affecting</td><td>github.com/golang/vscode-go/test/testdata/vuln</td>'),
56+
res.target
57+
);
5358
});
5459

55-
test('handles invalid input', async () => {
60+
test('handles empty input', async () => {
5661
const webviewPanel = _register(
5762
vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, {})
5863
);
@@ -74,6 +79,8 @@ suite('vulncheck result viewer tests', () => {
7479
assert.deepStrictEqual(res.type, 'snapshot-result', `want snapshot-result, got ${JSON.stringify(res)}`);
7580
assert(!res.target, res.target);
7681
});
82+
83+
// TODO: test corrupted/incomplete json file handling.
7784
});
7885

7986
function getMessage<R = { type: string; target?: string }>(webview: vscode.WebviewPanel): Promise<R> {
@@ -84,3 +91,44 @@ function getMessage<R = { type: string; target?: string }>(webview: vscode.Webvi
8491
});
8592
});
8693
}
94+
suite('fillAffectedPkgs', () => {
95+
test('compute from the first call stack entry', async () => {
96+
const data = JSON.parse(`{
97+
"Vuln": [{
98+
"CallStacks": [
99+
[
100+
{
101+
"Name": "github.com/golang/vscode-go/test/testdata/vuln.main",
102+
"URI": "file:///vuln/test.go",
103+
"Pos": { "line": 9, "character": 0 }
104+
},
105+
{
106+
"Name": "golang.org/x/text/language.Parse",
107+
"URI": "file:///foo/bar.go",
108+
"Pos": { "line": 227, "character": 0 }
109+
}
110+
]
111+
]}]}`);
112+
goVulncheck.fillAffectedPkgs(data.Vuln);
113+
assert.deepStrictEqual(data.Vuln[0].AffectedPkgs, ['github.com/golang/vscode-go/test/testdata/vuln']);
114+
});
115+
116+
test('callstacks missing', async () => {
117+
const data = JSON.parse('{ "Vuln": [{}] }');
118+
goVulncheck.fillAffectedPkgs(data.Vuln);
119+
assert.deepStrictEqual(data.Vuln[0].AffectedPkgs, []);
120+
});
121+
122+
test('callstacks empty', async () => {
123+
const data = JSON.parse('{ "Vuln": [{"CallStacks": []}] }');
124+
goVulncheck.fillAffectedPkgs(data.Vuln);
125+
assert.deepStrictEqual(data.Vuln[0].AffectedPkgs, []);
126+
});
127+
128+
test('first call stack entry is missing Name', async () => {
129+
const data = JSON.parse(`{
130+
"Vuln": [{ "CallStacks": [ [ { "URI": "file:///vuln/test.go" } ] ]}]}`);
131+
goVulncheck.fillAffectedPkgs(data.Vuln);
132+
assert.deepStrictEqual(data.Vuln[0].AffectedPkgs, []);
133+
});
134+
});

test/testdata/vuln/test.vulncheck.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"URL": "https://pkg.go.dev/vuln/GO-2021-0113",
1313
"CurrentVersion": "v0.0.0-20170915032832-14c0d48ead0c",
1414
"FixedVersion": "v0.3.7",
15+
"AffectedPkgs": ["github.com/golang/vscode-go/test/testdata/vuln"],
1516
"CallStacks": [
1617
[
1718
{
@@ -34,9 +35,6 @@
3435
],
3536
"CallStackSummaries": [
3637
"github.com/golang/vscode-go/test/testdata/vuln.main calls golang.org/x/text/language.Parse"
37-
],
38-
"AffectedPkgs": [
39-
"github.com/golang/vscode-go/test/testdata/vuln"
4038
]
4139
}
4240
],

0 commit comments

Comments
 (0)