src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

text.go (15427B)


      1 // Copyright 2022 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package scan
      6 
      7 import (
      8 	"fmt"
      9 	"io"
     10 	"sort"
     11 	"strings"
     12 
     13 	"golang.org/x/vuln/internal"
     14 	"golang.org/x/vuln/internal/govulncheck"
     15 	"golang.org/x/vuln/internal/osv"
     16 	"golang.org/x/vuln/internal/vulncheck"
     17 )
     18 
     19 type style int
     20 
     21 const (
     22 	defaultStyle = style(iota)
     23 	osvCalledStyle
     24 	osvImportedStyle
     25 	detailsStyle
     26 	sectionStyle
     27 	keyStyle
     28 	valueStyle
     29 )
     30 
     31 // NewtextHandler returns a handler that writes govulncheck output as text.
     32 func NewTextHandler(w io.Writer) *TextHandler {
     33 	return &TextHandler{w: w}
     34 }
     35 
     36 type TextHandler struct {
     37 	w         io.Writer
     38 	sbom      *govulncheck.SBOM
     39 	osvs      []*osv.Entry
     40 	findings  []*findingSummary
     41 	scanLevel govulncheck.ScanLevel
     42 	scanMode  govulncheck.ScanMode
     43 
     44 	err error
     45 
     46 	showColor   bool
     47 	showTraces  bool
     48 	showVersion bool
     49 	showVerbose bool
     50 }
     51 
     52 const (
     53 	detailsMessage = `For details, see https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck.`
     54 
     55 	binaryProgressMessage = `Scanning your binary for known vulnerabilities...`
     56 
     57 	noVulnsMessage = `No vulnerabilities found.`
     58 
     59 	noOtherVulnsMessage = `No other vulnerabilities found.`
     60 
     61 	verboseMessage = `'-show verbose' for more details`
     62 
     63 	symbolMessage = `'-scan symbol' for more fine grained vulnerability detection`
     64 )
     65 
     66 func (h *TextHandler) Flush() error {
     67 	if h.showVerbose {
     68 		h.printSBOM()
     69 	}
     70 	if len(h.findings) == 0 {
     71 		h.print(noVulnsMessage + "\n")
     72 	} else {
     73 		fixupFindings(h.osvs, h.findings)
     74 		counters := h.allVulns(h.findings)
     75 		h.summary(counters)
     76 	}
     77 	if h.err != nil {
     78 		return h.err
     79 	}
     80 	// We found vulnerabilities when the findings' level matches the scan level.
     81 	if (isCalled(h.findings) && h.scanLevel == govulncheck.ScanLevelSymbol) ||
     82 		(isImported(h.findings) && h.scanLevel == govulncheck.ScanLevelPackage) ||
     83 		(isRequired(h.findings) && h.scanLevel == govulncheck.ScanLevelModule) {
     84 		return errVulnerabilitiesFound
     85 	}
     86 
     87 	return nil
     88 }
     89 
     90 // Config writes version information only if --version was set.
     91 func (h *TextHandler) Config(config *govulncheck.Config) error {
     92 	h.scanLevel = config.ScanLevel
     93 	h.scanMode = config.ScanMode
     94 
     95 	if !h.showVersion {
     96 		return nil
     97 	}
     98 	if config.GoVersion != "" {
     99 		h.style(keyStyle, "Go: ")
    100 		h.print(config.GoVersion, "\n")
    101 	}
    102 	if config.ScannerName != "" {
    103 		h.style(keyStyle, "Scanner: ")
    104 		h.print(config.ScannerName)
    105 		if config.ScannerVersion != "" {
    106 			h.print(`@`, config.ScannerVersion)
    107 		}
    108 		h.print("\n")
    109 	}
    110 	if config.DB != "" {
    111 		h.style(keyStyle, "DB: ")
    112 		h.print(config.DB, "\n")
    113 		if config.DBLastModified != nil {
    114 			h.style(keyStyle, "DB updated: ")
    115 			h.print(*config.DBLastModified, "\n")
    116 		}
    117 	}
    118 	h.print("\n")
    119 	return h.err
    120 }
    121 
    122 func (h *TextHandler) SBOM(sbom *govulncheck.SBOM) error {
    123 	h.sbom = sbom
    124 	return nil
    125 }
    126 
    127 func (h *TextHandler) printSBOM() error {
    128 	if h.sbom == nil {
    129 		h.print("No packages matched the provided pattern.\n")
    130 		return nil
    131 	}
    132 
    133 	printed := false
    134 
    135 	for i, root := range h.sbom.Roots {
    136 		if i == 0 {
    137 			if len(h.sbom.Roots) > 1 {
    138 				h.print("The package pattern matched the following ", len(h.sbom.Roots), " root packages:\n")
    139 			} else {
    140 				h.print("The package pattern matched the following root package:\n")
    141 			}
    142 		}
    143 
    144 		h.print("  ", root, "\n")
    145 		printed = true
    146 	}
    147 	for i, mod := range h.sbom.Modules {
    148 		if i == 0 && mod.Path != "stdlib" {
    149 			h.print("Govulncheck scanned the following ", len(h.sbom.Modules)-1, " modules and the ", h.sbom.GoVersion, " standard library:\n")
    150 		}
    151 
    152 		if mod.Path == "stdlib" {
    153 			continue
    154 		}
    155 
    156 		h.print("  ", mod.Path)
    157 		if mod.Version != "" {
    158 			h.print("@", mod.Version)
    159 		}
    160 		h.print("\n")
    161 		printed = true
    162 	}
    163 	if printed {
    164 		h.print("\n")
    165 	}
    166 	return nil
    167 }
    168 
    169 // Progress writes progress updates during govulncheck execution.
    170 func (h *TextHandler) Progress(progress *govulncheck.Progress) error {
    171 	if h.showVerbose {
    172 		h.print(progress.Message, "\n\n")
    173 	}
    174 	return h.err
    175 }
    176 
    177 // OSV gathers osv entries to be written.
    178 func (h *TextHandler) OSV(entry *osv.Entry) error {
    179 	h.osvs = append(h.osvs, entry)
    180 	return nil
    181 }
    182 
    183 // Finding gathers vulnerability findings to be written.
    184 func (h *TextHandler) Finding(finding *govulncheck.Finding) error {
    185 	if err := validateFindings(finding); err != nil {
    186 		return err
    187 	}
    188 	h.findings = append(h.findings, newFindingSummary(finding))
    189 	return nil
    190 }
    191 
    192 func (h *TextHandler) allVulns(findings []*findingSummary) summaryCounters {
    193 	byVuln := groupByVuln(findings)
    194 	var called, imported, required [][]*findingSummary
    195 	mods := map[string]struct{}{}
    196 	stdlibCalled := false
    197 	for _, findings := range byVuln {
    198 		switch {
    199 		case isCalled(findings):
    200 			called = append(called, findings)
    201 			if isStdFindings(findings) {
    202 				stdlibCalled = true
    203 			} else {
    204 				mods[findings[0].Trace[0].Module] = struct{}{}
    205 			}
    206 		case isImported(findings):
    207 			imported = append(imported, findings)
    208 		default:
    209 			required = append(required, findings)
    210 		}
    211 	}
    212 
    213 	if h.scanLevel.WantSymbols() {
    214 		h.style(sectionStyle, "=== Symbol Results ===\n\n")
    215 		if len(called) == 0 {
    216 			h.print(noVulnsMessage, "\n\n")
    217 		}
    218 		for index, findings := range called {
    219 			h.vulnerability(index, findings)
    220 		}
    221 	}
    222 
    223 	if h.scanLevel == govulncheck.ScanLevelPackage || (h.scanLevel.WantPackages() && h.showVerbose) {
    224 		h.style(sectionStyle, "=== Package Results ===\n\n")
    225 		if len(imported) == 0 {
    226 			h.print(choose(!h.scanLevel.WantSymbols(), noVulnsMessage, noOtherVulnsMessage), "\n\n")
    227 		}
    228 		for index, findings := range imported {
    229 			h.vulnerability(index, findings)
    230 		}
    231 	}
    232 
    233 	if h.showVerbose || h.scanLevel == govulncheck.ScanLevelModule {
    234 		h.style(sectionStyle, "=== Module Results ===\n\n")
    235 		if len(required) == 0 {
    236 			h.print(choose(!h.scanLevel.WantPackages(), noVulnsMessage, noOtherVulnsMessage), "\n\n")
    237 		}
    238 		for index, findings := range required {
    239 			h.vulnerability(index, findings)
    240 		}
    241 	}
    242 
    243 	return summaryCounters{
    244 		VulnerabilitiesCalled:   len(called),
    245 		VulnerabilitiesImported: len(imported),
    246 		VulnerabilitiesRequired: len(required),
    247 		ModulesCalled:           len(mods),
    248 		StdlibCalled:            stdlibCalled,
    249 	}
    250 }
    251 
    252 func (h *TextHandler) vulnerability(index int, findings []*findingSummary) {
    253 	h.style(keyStyle, "Vulnerability")
    254 	h.print(" #", index+1, ": ")
    255 	if isCalled(findings) {
    256 		h.style(osvCalledStyle, findings[0].OSV.ID)
    257 	} else {
    258 		h.style(osvImportedStyle, findings[0].OSV.ID)
    259 	}
    260 	h.print("\n")
    261 	h.style(detailsStyle)
    262 	description := findings[0].OSV.Summary
    263 	if description == "" {
    264 		description = findings[0].OSV.Details
    265 	}
    266 	h.wrap("    ", description, 80)
    267 	h.style(defaultStyle)
    268 	h.print("\n")
    269 	h.style(keyStyle, "  More info:")
    270 	h.print(" ", findings[0].OSV.DatabaseSpecific.URL, "\n")
    271 
    272 	byModule := groupByModule(findings)
    273 	first := true
    274 	for _, module := range byModule {
    275 		// Note: there can be several findingSummaries for the same vulnerability
    276 		// emitted during streaming for different scan levels.
    277 
    278 		// The module is same for all finding summaries.
    279 		lastFrame := module[0].Trace[0]
    280 		mod := lastFrame.Module
    281 		// For stdlib, try to show package path as module name where
    282 		// the scan level allows it.
    283 		// TODO: should this be done in byModule as well?
    284 		path := lastFrame.Module
    285 		if stdPkg := h.pkg(module); path == internal.GoStdModulePath && stdPkg != "" {
    286 			path = stdPkg
    287 		}
    288 		// All findings on a module are found and fixed at the same version
    289 		foundVersion := moduleVersionString(lastFrame.Module, lastFrame.Version)
    290 		fixedVersion := moduleVersionString(lastFrame.Module, module[0].FixedVersion)
    291 		if !first {
    292 			h.print("\n")
    293 		}
    294 		first = false
    295 		h.print("  ")
    296 		if mod == internal.GoStdModulePath {
    297 			h.print("Standard library")
    298 		} else {
    299 			h.style(keyStyle, "Module: ")
    300 			h.print(mod)
    301 		}
    302 		h.print("\n    ")
    303 		h.style(keyStyle, "Found in: ")
    304 		h.print(path, "@", foundVersion, "\n    ")
    305 		h.style(keyStyle, "Fixed in: ")
    306 		if fixedVersion != "" {
    307 			h.print(path, "@", fixedVersion)
    308 		} else {
    309 			h.print("N/A")
    310 		}
    311 		h.print("\n")
    312 		platforms := platforms(mod, module[0].OSV)
    313 		if len(platforms) > 0 {
    314 			h.style(keyStyle, "    Platforms: ")
    315 			for ip, p := range platforms {
    316 				if ip > 0 {
    317 					h.print(", ")
    318 				}
    319 				h.print(p)
    320 			}
    321 			h.print("\n")
    322 		}
    323 		h.traces(module)
    324 	}
    325 	h.print("\n")
    326 }
    327 
    328 // pkg gives the package information for findings summaries
    329 // if one exists. This is only used to print package path
    330 // instead of a module for stdlib vulnerabilities at symbol
    331 // and package scan level.
    332 func (h *TextHandler) pkg(summaries []*findingSummary) string {
    333 	for _, f := range summaries {
    334 		if pkg := f.Trace[0].Package; pkg != "" {
    335 			return pkg
    336 		}
    337 	}
    338 	return ""
    339 }
    340 
    341 // traces prints out the most precise trace information
    342 // found in the given summaries.
    343 func (h *TextHandler) traces(traces []*findingSummary) {
    344 	// Sort the traces by the vulnerable symbol. This
    345 	// guarantees determinism since we are currently
    346 	// showing only one trace per symbol.
    347 	sort.SliceStable(traces, func(i, j int) bool {
    348 		return symbol(traces[i].Trace[0], true) < symbol(traces[j].Trace[0], true)
    349 	})
    350 
    351 	// compacts are finding summaries with compact traces
    352 	// suitable for non-verbose textual output. Currently,
    353 	// only traces produced by symbol analysis.
    354 	var compacts []*findingSummary
    355 	for _, t := range traces {
    356 		if t.Compact != "" {
    357 			compacts = append(compacts, t)
    358 		}
    359 	}
    360 
    361 	// binLimit is a limit on the number of binary traces
    362 	// to show. Traces for binaries are less interesting
    363 	// as users cannot act on them and they can hence
    364 	// spam users.
    365 	const binLimit = 5
    366 	binary := h.scanMode == govulncheck.ScanModeBinary
    367 	for i, entry := range compacts {
    368 		if i == 0 {
    369 			if binary {
    370 				h.style(keyStyle, "    Vulnerable symbols found:\n")
    371 			} else {
    372 				h.style(keyStyle, "    Example traces found:\n")
    373 			}
    374 		}
    375 
    376 		// skip showing all symbols in binary mode unless '-show traces' is on.
    377 		if binary && (i+1) > binLimit && !h.showTraces {
    378 			h.print("      Use '-show traces' to see the other ", len(compacts)-binLimit, " found symbols\n")
    379 			break
    380 		}
    381 
    382 		h.print("      #", i+1, ": ")
    383 
    384 		if !h.showTraces { // show summarized traces
    385 			h.print(entry.Compact, "\n")
    386 			continue
    387 		}
    388 
    389 		if binary {
    390 			// There are no call stacks in binary mode
    391 			// so just show the full symbol name.
    392 			h.print(symbol(entry.Trace[0], false), "\n")
    393 		} else {
    394 			h.print("for function ", symbol(entry.Trace[0], false), "\n")
    395 			for i := len(entry.Trace) - 1; i >= 0; i-- {
    396 				t := entry.Trace[i]
    397 				h.print("        ")
    398 				h.print(symbolName(t))
    399 				if t.Position != nil {
    400 					h.print(" @ ", symbolPath(t))
    401 				}
    402 				h.print("\n")
    403 			}
    404 		}
    405 	}
    406 }
    407 
    408 // symbolPath returns a user-friendly path to a symbol.
    409 func symbolPath(t *govulncheck.Frame) string {
    410 	// Add module path prefix to symbol paths to be more
    411 	// explicit to which module the symbols belong to.
    412 	return t.Module + "/" + posToString(t.Position)
    413 }
    414 
    415 func (h *TextHandler) summary(c summaryCounters) {
    416 	// print short summary of findings identified at the desired level of scan precision
    417 	var vulnCount int
    418 	h.print("Your code ", choose(h.scanLevel.WantSymbols(), "is", "may be"), " affected by ")
    419 	switch h.scanLevel {
    420 	case govulncheck.ScanLevelSymbol:
    421 		vulnCount = c.VulnerabilitiesCalled
    422 	case govulncheck.ScanLevelPackage:
    423 		vulnCount = c.VulnerabilitiesImported
    424 	case govulncheck.ScanLevelModule:
    425 		vulnCount = c.VulnerabilitiesRequired
    426 	}
    427 	h.style(valueStyle, vulnCount)
    428 	h.print(choose(vulnCount == 1, ` vulnerability`, ` vulnerabilities`))
    429 	if h.scanLevel.WantSymbols() {
    430 		h.print(choose(c.ModulesCalled > 0 || c.StdlibCalled, ` from `, ``))
    431 		if c.ModulesCalled > 0 {
    432 			h.style(valueStyle, c.ModulesCalled)
    433 			h.print(choose(c.ModulesCalled == 1, ` module`, ` modules`))
    434 		}
    435 		if c.StdlibCalled {
    436 			if c.ModulesCalled != 0 {
    437 				h.print(` and `)
    438 			}
    439 			h.print(`the Go standard library`)
    440 		}
    441 	}
    442 	h.print(".\n")
    443 
    444 	// print summary for vulnerabilities found at other levels of scan precision
    445 	if other := h.summaryOtherVulns(c); other != "" {
    446 		h.wrap("", other, 80)
    447 		h.print("\n")
    448 	}
    449 
    450 	// print suggested flags for more/better info depending on scan level and if in verbose mode
    451 	if sugg := h.summarySuggestion(); sugg != "" {
    452 		h.wrap("", sugg, 80)
    453 		h.print("\n")
    454 	}
    455 }
    456 
    457 func (h *TextHandler) summaryOtherVulns(c summaryCounters) string {
    458 	var summary strings.Builder
    459 	if c.VulnerabilitiesRequired+c.VulnerabilitiesImported == 0 {
    460 		summary.WriteString("This scan found no other vulnerabilities in ")
    461 		if h.scanLevel.WantSymbols() {
    462 			summary.WriteString("packages you import or ")
    463 		}
    464 		summary.WriteString("modules you require.")
    465 	} else {
    466 		summary.WriteString(choose(h.scanLevel.WantPackages(), "This scan also found ", ""))
    467 		if h.scanLevel.WantSymbols() {
    468 			summary.WriteString(fmt.Sprint(c.VulnerabilitiesImported))
    469 			summary.WriteString(choose(c.VulnerabilitiesImported == 1, ` vulnerability `, ` vulnerabilities `))
    470 			summary.WriteString("in packages you import and ")
    471 		}
    472 		if h.scanLevel.WantPackages() {
    473 			summary.WriteString(fmt.Sprint(c.VulnerabilitiesRequired))
    474 			summary.WriteString(choose(c.VulnerabilitiesRequired == 1, ` vulnerability `, ` vulnerabilities `))
    475 			summary.WriteString("in modules you require")
    476 			summary.WriteString(choose(h.scanLevel.WantSymbols(), ", but your code doesn't appear to call these vulnerabilities.", "."))
    477 		}
    478 	}
    479 	return summary.String()
    480 }
    481 
    482 func (h *TextHandler) summarySuggestion() string {
    483 	var sugg strings.Builder
    484 	switch h.scanLevel {
    485 	case govulncheck.ScanLevelSymbol:
    486 		if !h.showVerbose {
    487 			sugg.WriteString("Use " + verboseMessage + ".")
    488 		}
    489 	case govulncheck.ScanLevelPackage:
    490 		sugg.WriteString("Use " + symbolMessage)
    491 		if !h.showVerbose {
    492 			sugg.WriteString(" and " + verboseMessage)
    493 		}
    494 		sugg.WriteString(".")
    495 	case govulncheck.ScanLevelModule:
    496 		sugg.WriteString("Use " + symbolMessage + ".")
    497 	}
    498 	return sugg.String()
    499 }
    500 
    501 func (h *TextHandler) style(style style, values ...any) {
    502 	if h.showColor {
    503 		switch style {
    504 		default:
    505 			h.print(colorReset)
    506 		case osvCalledStyle:
    507 			h.print(colorBold, fgRed)
    508 		case osvImportedStyle:
    509 			h.print(colorBold, fgGreen)
    510 		case detailsStyle:
    511 			h.print(colorFaint)
    512 		case sectionStyle:
    513 			h.print(fgBlue)
    514 		case keyStyle:
    515 			h.print(colorFaint, fgYellow)
    516 		case valueStyle:
    517 			h.print(colorBold, fgCyan)
    518 		}
    519 	}
    520 	h.print(values...)
    521 	if h.showColor && len(values) > 0 {
    522 		h.print(colorReset)
    523 	}
    524 }
    525 
    526 func (h *TextHandler) print(values ...any) int {
    527 	total, w := 0, 0
    528 	for _, v := range values {
    529 		if h.err != nil {
    530 			return total
    531 		}
    532 		// do we need to specialize for some types, like time?
    533 		w, h.err = fmt.Fprint(h.w, v)
    534 		total += w
    535 	}
    536 	return total
    537 }
    538 
    539 // wrap wraps s to fit in maxWidth by breaking it into lines at whitespace. If a
    540 // single word is longer than maxWidth, it is retained as its own line.
    541 func (h *TextHandler) wrap(indent string, s string, maxWidth int) {
    542 	w := 0
    543 	for _, f := range strings.Fields(s) {
    544 		if w > 0 && w+len(f)+1 > maxWidth {
    545 			// line would be too long with this word
    546 			h.print("\n")
    547 			w = 0
    548 		}
    549 		if w == 0 {
    550 			// first field on line, indent
    551 			w = h.print(indent)
    552 		} else {
    553 			// not first word, space separate
    554 			w += h.print(" ")
    555 		}
    556 		// now write the word
    557 		w += h.print(f)
    558 	}
    559 }
    560 
    561 func choose[t any](b bool, yes, no t) t {
    562 	if b {
    563 		return yes
    564 	}
    565 	return no
    566 }
    567 
    568 func isStdFindings(findings []*findingSummary) bool {
    569 	for _, f := range findings {
    570 		if vulncheck.IsStdPackage(f.Trace[0].Package) || f.Trace[0].Module == internal.GoStdModulePath {
    571 			return true
    572 		}
    573 	}
    574 	return false
    575 }