Skip to content

Commit

Permalink
src/goVulncheck2: output govulncheck progress and result
Browse files Browse the repository at this point in the history
This is done by intercepting the progress report and issuing
the followup gopls command to retrieve the govulncheck result
kept in the gopls.

Change-Id: Ibdfdc9a6acc639eecdff2a5b39d3ef688ad7f421
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/454137
Run-TryBot: Hyang-Ah Hana Kim <[email protected]>
Reviewed-by: Jamal Carvalho <[email protected]>
TryBot-Result: kokoro <[email protected]>
  • Loading branch information
hyangah committed Dec 7, 2022
1 parent aff24c5 commit 3bcbaaf
Show file tree
Hide file tree
Showing 5 changed files with 545 additions and 5 deletions.
266 changes: 266 additions & 0 deletions src/goVulncheck2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*---------------------------------------------------------
* Copyright 2022 The Go Authors. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/

function moduleVersion(mod: string, ver: string | undefined) {
if (!ver) {
return 'N/A';
}
if (mod === 'stdlib') {
return `go${ver.replace(/^(v|go)/, '')}`;
}
return `${mod}@${ver}`;
}

// writeVulns generates human-readable vulnerability report from the VulncheckReport
// and write to the outputChannel.
export function writeVulns(
res: VulncheckReport | undefined | null,
outputChannel: { appendLine(value: string): void }
) {
outputChannel.appendLine('');

if (!res) {
outputChannel.appendLine('Error - invalid vulncheck result.'); // TODO(hyangah): ask to open an issue.
return;
}
if (!res.Vulns || res.Vulns.length === 0) {
outputChannel.appendLine('No vulnerability found.');
return;
}

const affecting = res.Vulns.filter((v) => {
return v.Modules?.some((m) => {
return m.Packages?.some((p) => {
return p.CallStacks?.some((cs) => {
return cs.Frames && cs.Frames.length > 0;
});
});
});
});
const unaffecting = res.Vulns.filter((v) => !affecting.includes(v));

switch (affecting.length) {
case 0:
outputChannel.appendLine('No vulnerability found.');
break;
case 1:
outputChannel.appendLine(`Found ${affecting.length} affecting vulnerability.`);
outputChannel.appendLine('-'.repeat(80));
break;
default:
outputChannel.appendLine(`Found ${affecting.length} affecting vulnerabilities.`);
outputChannel.appendLine('-'.repeat(80));
break;
}

affecting.forEach((vuln) => {
outputChannel.appendLine(`⚠ ${vuln.OSV.id} (https://pkg.go.dev/vuln/${vuln.OSV.id})`);
const desc = (vuln.OSV.details || '').trimRight();
const aliases = vuln.OSV.aliases?.length ? ` (${vuln.OSV.aliases.join(', ')})` : '';
outputChannel.appendLine(`\n${desc}${aliases}\n`);
vuln.Modules?.forEach((mod) => {
outputChannel.appendLine(`Found Version: ${moduleVersion(mod.Path, mod.FoundVersion)}`);
outputChannel.appendLine(`Fixed Version: ${moduleVersion(mod.Path, mod.FixedVersion)}`);
mod.Packages?.forEach((pkg) => {
outputChannel.appendLine('');
pkg.CallStacks?.forEach((cs, index) => {
// TODO: the position info embedded in the cs.Summary is relative to
// the directory gopls ran the vulnchek.
// Instead replace with workspace-relative paths.
outputChannel.appendLine(`- ${cs.Summary}`);
// Print the first trace (index === 0) as an example.
// TODO(hyangah): allow users to see example traces for all detected vulnerable symbols.
if (index === 0 && cs.Frames) {
const last = cs.Frames.length - 1;
cs.Frames?.forEach((f, index) => {
// Skip the last frame that just carries the vulnerable symbol.
// This info is already included in cs.Summary.
if (last === index) return;
const line = f.Position?.Line || 1;
// TODO: shorten f.Position.Filename (e.g. workspace relative path, and home directory ~ relative path)
const pos = f.Position?.Filename ? `${f.Position.Filename}:${line}` : ' - ';
const name = f.RecvType ? `${f.RecvType}.${f.FuncName}` : `${f.PkgPath}.${f.FuncName}`;
outputChannel.appendLine(`\t${name}\n\t\t(${pos})`);
});
}
});
});
});
outputChannel.appendLine('-'.repeat(80));
});

if (unaffecting.length) {
outputChannel.appendLine(`
# 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 https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck for details.
`);

switch (unaffecting.length) {
case 1:
outputChannel.appendLine(`Found ${unaffecting.length} unused vulnerability.`);
break;
default:
outputChannel.appendLine(`Found ${unaffecting.length} unused vulnerabilities.`);
break;
}
outputChannel.appendLine('-'.repeat(80));
}

unaffecting.forEach((vuln) => {
outputChannel.appendLine(`ⓘ ${vuln.OSV.id} (https://pkg.go.dev/vuln/${vuln.OSV.id})`);
const desc = (vuln.OSV.details || '').trimRight();
const aliases = vuln.OSV.aliases?.length ? ` (${vuln.OSV.aliases.join(', ')})` : '';
outputChannel.appendLine(`\n${desc}${aliases}\n`);
vuln.Modules?.forEach((mod) => {
outputChannel.appendLine(`Found Version: ${moduleVersion(mod.Path, mod.FoundVersion)}`);
outputChannel.appendLine(`Fixed Version: ${moduleVersion(mod.Path, mod.FixedVersion)}`);
mod.Packages?.forEach((pkg) => {
outputChannel.appendLine(`Package: ${pkg.Path}`);
});
});
outputChannel.appendLine('-'.repeat(80));
});
}

// VulncheckReport is the JSON data type of gopls's vulncheck result.
export interface VulncheckReport {
// Vulns populated by gopls vulncheck run.
Vulns?: Vuln[];

Mode?: 'govulncheck' | 'imports';
}

// Vuln represents a single OSV entry.
interface Vuln {
// OSV contains all data from the OSV entry for this vulnerability.
OSV: OSVEntry;

// Modules contains all of the modules in the OSV entry where a
// vulnerable package is imported by the target source code or binary.
//
// For example, a module M with two packages M/p1 and M/p2, where only p1
// is vulnerable, will appear in this list if and only if p1 is imported by
// the target source code or binary.
Modules: Module[];

AffectedPackages?: string[];
}

interface OSVEntry {
id: string;
published?: string;
aliases?: string[];
details?: string;
affected?: Affected[];
}

interface Affected {
package: Package;
ecosystem_specific?: EcosystemSpecific;
}

interface EcosystemSpecificImport {
path: string;
goos?: string[];
goarch?: string[];
symbols?: string[];
}

interface EcosystemSpecific {
imports?: EcosystemSpecificImport[];
}

interface Package {
name: string;
}

interface Module {
// Path is the module path of the module containing the vulnerability.
//
// Importable packages in the standard library will have the path "stdlib".
Path: string;

// FoundVersion is the module version where the vulnerability was found.
FoundVersion?: string;

// FixedVersion is the module version where the vulnerability was
// fixed. If there are multiple fixed versions in the OSV report, this will
// be the latest fixed version.
//
// This is empty if a fix is not available.
FixedVersion?: string;

// Packages contains all the vulnerable packages in OSV entry that are
// imported by the target source code or binary.
//
// For example, given a module M with two packages M/p1 and M/p2, where
// both p1 and p2 are vulnerable, p1 and p2 will each only appear in this
// list they are individually imported by the target source code or binary.
Packages?: Package[];
}

interface Package {
// Path is the import path of the package containing the vulnerability.
Path: string;

// CallStacks contains a representative call stack for each
// vulnerable symbol that is called.
//
// For vulnerabilities found from binary analysis, only CallStack.Symbol
// will be provided.
//
// For non-affecting vulnerabilities reported from the source mode
// analysis, this will be empty.
CallStacks?: CallStack[];
}

interface CallStack {
// Symbol is the name of the detected vulnerable function
// or method.
//
// This follows the naming convention in the OSV report.
Symbol?: string;

// Summary is a one-line description of the callstack, used by the
// default govulncheck mode.
//
// Example: module3.main calls github.com/shiyanhui/dht.DHT.Run
Summary?: string;

// Frames contains an entry for each stack in the call stack.
//
// Frames are sorted starting from the entry point to the
// imported vulnerable symbol. The last frame in Frames should match
// Symbol.
Frames?: StackFrame[];
}

interface StackFrame {
// PackagePath is the import path.
PkgPath: string;

// FuncName is the function name.
FuncName?: string;

// RecvType is the fully qualified receiver type,
// if the called symbol is a method.
//
// The client can create the final symbol name by
// prepending RecvType to FuncName.
RecvType?: string;

// Position describes an arbitrary source position
// including the file, line, and column location.
// A Position is valid if the line number is > 0.
Position?: Position;
}

interface Position {
Filename?: string; // filename, if any
Offset?: number; // offset, starting at 0
Line?: number; // line number, starting at 1
Column?: number; // column number, starting at 1 (byte count)
}
Loading

0 comments on commit 3bcbaaf

Please sign in to comment.