src

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

handler.go (11841B)


      1 // Copyright 2023 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 sarif
      6 
      7 import (
      8 	"encoding/json"
      9 	"fmt"
     10 	"io"
     11 	"path/filepath"
     12 	"sort"
     13 
     14 	"golang.org/x/vuln/internal"
     15 	"golang.org/x/vuln/internal/govulncheck"
     16 	"golang.org/x/vuln/internal/osv"
     17 	"golang.org/x/vuln/internal/traces"
     18 )
     19 
     20 // handler for sarif output.
     21 type handler struct {
     22 	w    io.Writer
     23 	cfg  *govulncheck.Config
     24 	osvs map[string]*osv.Entry
     25 	// findings contains same-level findings for an
     26 	// OSV at the most precise level of granularity
     27 	// available. This means, for instance, that if
     28 	// an osv is indeed called, then all findings for
     29 	// the osv will have call stack info.
     30 	findings map[string][]*govulncheck.Finding
     31 }
     32 
     33 func NewHandler(w io.Writer) *handler {
     34 	return &handler{
     35 		w:        w,
     36 		osvs:     make(map[string]*osv.Entry),
     37 		findings: make(map[string][]*govulncheck.Finding),
     38 	}
     39 }
     40 
     41 func (h *handler) Config(c *govulncheck.Config) error {
     42 	h.cfg = c
     43 	return nil
     44 }
     45 
     46 func (h *handler) Progress(p *govulncheck.Progress) error {
     47 	return nil // not needed by sarif
     48 }
     49 
     50 func (h *handler) SBOM(s *govulncheck.SBOM) error {
     51 	return nil // not needed by sarif
     52 }
     53 
     54 func (h *handler) OSV(e *osv.Entry) error {
     55 	h.osvs[e.ID] = e
     56 	return nil
     57 }
     58 
     59 // moreSpecific favors a call finding over a non-call
     60 // finding and a package finding over a module finding.
     61 func moreSpecific(f1, f2 *govulncheck.Finding) int {
     62 	if len(f1.Trace) > 1 && len(f2.Trace) > 1 {
     63 		// Both are call stack findings.
     64 		return 0
     65 	}
     66 	if len(f1.Trace) > 1 {
     67 		return -1
     68 	}
     69 	if len(f2.Trace) > 1 {
     70 		return 1
     71 	}
     72 
     73 	fr1, fr2 := f1.Trace[0], f2.Trace[0]
     74 	if fr1.Function != "" && fr2.Function == "" {
     75 		return -1
     76 	}
     77 	if fr1.Function == "" && fr2.Function != "" {
     78 		return 1
     79 	}
     80 	if fr1.Package != "" && fr2.Package == "" {
     81 		return -1
     82 	}
     83 	if fr1.Package == "" && fr2.Package != "" {
     84 		return -1
     85 	}
     86 	return 0 // findings always have module info
     87 }
     88 
     89 func (h *handler) Finding(f *govulncheck.Finding) error {
     90 	fs := h.findings[f.OSV]
     91 	if len(fs) == 0 {
     92 		fs = []*govulncheck.Finding{f}
     93 	} else {
     94 		if ms := moreSpecific(f, fs[0]); ms == -1 {
     95 			// The new finding is more specific, so we need
     96 			// to erase existing findings and add the new one.
     97 			fs = []*govulncheck.Finding{f}
     98 		} else if ms == 0 {
     99 			// The new finding is equal to an existing one and
    100 			// because of the invariant on h.findings, it is
    101 			// also equal to all existing ones.
    102 			fs = append(fs, f)
    103 		}
    104 		// Otherwise, the new finding is at a less precise level.
    105 	}
    106 	h.findings[f.OSV] = fs
    107 	return nil
    108 }
    109 
    110 // Flush is used to print out to w the sarif json output.
    111 // This is needed as sarif is not streamed.
    112 func (h *handler) Flush() error {
    113 	sLog := toSarif(h)
    114 	s, err := json.MarshalIndent(sLog, "", "  ")
    115 	if err != nil {
    116 		return err
    117 	}
    118 	h.w.Write(s)
    119 	return nil
    120 }
    121 
    122 func toSarif(h *handler) Log {
    123 	cfg := h.cfg
    124 	r := Run{
    125 		Tool: Tool{
    126 			Driver: Driver{
    127 				Name:           cfg.ScannerName,
    128 				Version:        cfg.ScannerVersion,
    129 				InformationURI: "https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck",
    130 				Properties:     *cfg,
    131 				Rules:          rules(h),
    132 			},
    133 		},
    134 		Results: results(h),
    135 	}
    136 
    137 	return Log{
    138 		Version: "2.1.0",
    139 		Schema:  "https://json.schemastore.org/sarif-2.1.0.json",
    140 		Runs:    []Run{r},
    141 	}
    142 }
    143 
    144 func rules(h *handler) []Rule {
    145 	rs := make([]Rule, 0, len(h.findings)) // must not be nil
    146 	for id := range h.findings {
    147 		osv := h.osvs[id]
    148 		// s is either summary if it exists, or details
    149 		// otherwise. Govulncheck text does the same.
    150 		s := osv.Summary
    151 		if s == "" {
    152 			s = osv.Details
    153 		}
    154 		rs = append(rs, Rule{
    155 			ID:               osv.ID,
    156 			ShortDescription: Description{Text: fmt.Sprintf("[%s] %s", osv.ID, s)},
    157 			FullDescription:  Description{Text: s},
    158 			HelpURI:          fmt.Sprintf("https://pkg.go.dev/vuln/%s", osv.ID),
    159 			Help:             Description{Text: osv.Details},
    160 			Properties:       RuleTags{Tags: tags(osv)},
    161 		})
    162 	}
    163 	sort.SliceStable(rs, func(i, j int) bool { return rs[i].ID < rs[j].ID })
    164 	return rs
    165 }
    166 
    167 // tags returns an slice of zero or
    168 // more aliases of o.
    169 func tags(o *osv.Entry) []string {
    170 	if len(o.Aliases) > 0 {
    171 		return o.Aliases
    172 	}
    173 	return []string{} // must not be nil
    174 }
    175 
    176 func results(h *handler) []Result {
    177 	results := make([]Result, 0, len(h.findings)) // must not be nil
    178 	for osv, fs := range h.findings {
    179 		var locs []Location
    180 		if h.cfg.ScanMode != govulncheck.ScanModeBinary {
    181 			// Attach result to the go.mod file for source analysis.
    182 			// But there is no such place for binaries.
    183 			locs = []Location{{PhysicalLocation: PhysicalLocation{
    184 				ArtifactLocation: ArtifactLocation{
    185 					URI:       "go.mod",
    186 					URIBaseID: SrcRootID,
    187 				},
    188 				Region: Region{StartLine: 1}, // for now, point to the first line
    189 			},
    190 				Message: Description{Text: fmt.Sprintf("Findings for vulnerability %s", osv)}, // not having a message here results in an invalid sarif
    191 			}}
    192 		}
    193 
    194 		res := Result{
    195 			RuleID:    osv,
    196 			Level:     level(fs[0], h.cfg),
    197 			Message:   Description{Text: resultMessage(fs, h.cfg)},
    198 			Stacks:    stacks(h, fs),
    199 			CodeFlows: codeFlows(h, fs),
    200 			Locations: locs,
    201 		}
    202 		results = append(results, res)
    203 	}
    204 	sort.SliceStable(results, func(i, j int) bool { return results[i].RuleID < results[j].RuleID }) // for deterministic output
    205 	return results
    206 }
    207 
    208 func resultMessage(findings []*govulncheck.Finding, cfg *govulncheck.Config) string {
    209 	// We can infer the findings' level by just looking at the
    210 	// top trace frame of any finding.
    211 	frame := findings[0].Trace[0]
    212 	uniqueElems := make(map[string]bool)
    213 	if frame.Function == "" && frame.Package == "" { // module level findings
    214 		for _, f := range findings {
    215 			uniqueElems[f.Trace[0].Module] = true
    216 		}
    217 	} else { // symbol and package level findings
    218 		for _, f := range findings {
    219 			uniqueElems[f.Trace[0].Package] = true
    220 		}
    221 	}
    222 	var elems []string
    223 	for e := range uniqueElems {
    224 		elems = append(elems, e)
    225 	}
    226 	sort.Strings(elems)
    227 
    228 	l := len(elems)
    229 	elemList := list(elems)
    230 	main, addition := "", ""
    231 	const runCallAnalysis = "Run the call-level analysis to understand whether your code actually calls the vulnerabilities."
    232 	switch {
    233 	case frame.Function != "":
    234 		main = fmt.Sprintf("calls vulnerable functions in %d package%s (%s).", l, choose("", "s", l == 1), elemList)
    235 	case frame.Package != "":
    236 		main = fmt.Sprintf("imports %d vulnerable package%s (%s)", l, choose("", "s", l == 1), elemList)
    237 		addition = choose(", but doesn’t appear to call any of the vulnerable symbols.", ". "+runCallAnalysis, cfg.ScanLevel.WantSymbols())
    238 	default:
    239 		main = fmt.Sprintf("depends on %d vulnerable module%s (%s)", l, choose("", "s", l == 1), elemList)
    240 		informational := ", but doesn't appear to " + choose("call", "import", cfg.ScanLevel.WantSymbols()) + " any of the vulnerable symbols."
    241 		addition = choose(informational, ". "+runCallAnalysis, cfg.ScanLevel.WantPackages())
    242 	}
    243 
    244 	return fmt.Sprintf("Your code %s%s", main, addition)
    245 }
    246 
    247 const (
    248 	errorLevel         = "error"
    249 	warningLevel       = "warning"
    250 	informationalLevel = "note"
    251 )
    252 
    253 func level(f *govulncheck.Finding, cfg *govulncheck.Config) string {
    254 	fr := f.Trace[0]
    255 	switch {
    256 	case cfg.ScanLevel.WantSymbols():
    257 		if fr.Function != "" {
    258 			return errorLevel
    259 		}
    260 		if fr.Package != "" {
    261 			return warningLevel
    262 		}
    263 		return informationalLevel
    264 	case cfg.ScanLevel.WantPackages():
    265 		if fr.Package != "" {
    266 			return errorLevel
    267 		}
    268 		return warningLevel
    269 	default:
    270 		return errorLevel
    271 	}
    272 }
    273 
    274 func stacks(h *handler, fs []*govulncheck.Finding) []Stack {
    275 	if fs[0].Trace[0].Function == "" { // not call level findings
    276 		return nil
    277 	}
    278 
    279 	var stacks []Stack
    280 	for _, f := range fs {
    281 		stacks = append(stacks, stack(h, f))
    282 	}
    283 	// Sort stacks for deterministic output. We sort by message
    284 	// which is effectively sorting by full symbol name. The
    285 	// performance should not be an issue here.
    286 	sort.SliceStable(stacks, func(i, j int) bool { return stacks[i].Message.Text < stacks[j].Message.Text })
    287 	return stacks
    288 }
    289 
    290 // stack transforms call stack in f to a sarif stack.
    291 func stack(h *handler, f *govulncheck.Finding) Stack {
    292 	trace := f.Trace
    293 	top := trace[len(trace)-1] // belongs to top level module
    294 
    295 	frames := make([]Frame, 0, len(trace)) // must not be nil
    296 	for i := len(trace) - 1; i >= 0; i-- { // vulnerable symbol is at the top frame
    297 		frame := trace[i]
    298 		pos := govulncheck.Position{Line: 1, Column: 1}
    299 		if frame.Position != nil {
    300 			pos = *frame.Position
    301 		}
    302 
    303 		sf := Frame{
    304 			Module:   frame.Module + "@" + frame.Version,
    305 			Location: Location{Message: Description{Text: symbol(frame)}}, // show the (full) symbol name
    306 		}
    307 		file, base := fileURIInfo(pos.Filename, top.Module, frame.Module, frame.Version)
    308 		if h.cfg.ScanMode != govulncheck.ScanModeBinary {
    309 			sf.Location.PhysicalLocation = PhysicalLocation{
    310 				ArtifactLocation: ArtifactLocation{
    311 					URI:       file,
    312 					URIBaseID: base,
    313 				},
    314 				Region: Region{
    315 					StartLine:   pos.Line,
    316 					StartColumn: pos.Column,
    317 				},
    318 			}
    319 		}
    320 		frames = append(frames, sf)
    321 	}
    322 
    323 	return Stack{
    324 		Frames:  frames,
    325 		Message: Description{Text: fmt.Sprintf("A call stack for vulnerable function %s", symbol(trace[0]))},
    326 	}
    327 }
    328 
    329 func codeFlows(h *handler, fs []*govulncheck.Finding) []CodeFlow {
    330 	if fs[0].Trace[0].Function == "" { // not call level findings
    331 		return nil
    332 	}
    333 
    334 	// group call stacks per symbol. There should
    335 	// be one call stack currently per symbol, but
    336 	// this might change in the future.
    337 	m := make(map[govulncheck.Frame][]*govulncheck.Finding)
    338 	for _, f := range fs {
    339 		// fr.Position is currently the position
    340 		// of the definition of the vuln symbol
    341 		fr := *f.Trace[0]
    342 		m[fr] = append(m[fr], f)
    343 	}
    344 
    345 	var codeFlows []CodeFlow
    346 	for fr, fs := range m {
    347 		tfs := threadFlows(h, fs)
    348 		codeFlows = append(codeFlows, CodeFlow{
    349 			ThreadFlows: tfs,
    350 			// TODO: should we instead show the message from govulncheck text output?
    351 			Message: Description{Text: fmt.Sprintf("A summarized code flow for vulnerable function %s", symbol(&fr))},
    352 		})
    353 	}
    354 	// Sort flows for deterministic output. We sort by message
    355 	// which is effectively sorting by full symbol name. The
    356 	// performance should not be an issue here.
    357 	sort.SliceStable(codeFlows, func(i, j int) bool { return codeFlows[i].Message.Text < codeFlows[j].Message.Text })
    358 	return codeFlows
    359 }
    360 
    361 func threadFlows(h *handler, fs []*govulncheck.Finding) []ThreadFlow {
    362 	tfs := make([]ThreadFlow, 0, len(fs)) // must not be nil
    363 	for _, f := range fs {
    364 		trace := traces.Compact(f)
    365 		top := trace[len(trace)-1] // belongs to top level module
    366 
    367 		var tf []ThreadFlowLocation
    368 		for i := len(trace) - 1; i >= 0; i-- { // vulnerable symbol is at the top frame
    369 			// TODO: should we, similar to govulncheck text output, only
    370 			// mention three elements of the compact trace?
    371 			frame := trace[i]
    372 			pos := govulncheck.Position{Line: 1, Column: 1}
    373 			if frame.Position != nil {
    374 				pos = *frame.Position
    375 			}
    376 
    377 			tfl := ThreadFlowLocation{
    378 				Module:   frame.Module + "@" + frame.Version,
    379 				Location: Location{Message: Description{Text: symbol(frame)}}, // show the (full) symbol name
    380 			}
    381 			file, base := fileURIInfo(pos.Filename, top.Module, frame.Module, frame.Version)
    382 			if h.cfg.ScanMode != govulncheck.ScanModeBinary {
    383 				tfl.Location.PhysicalLocation = PhysicalLocation{
    384 					ArtifactLocation: ArtifactLocation{
    385 						URI:       file,
    386 						URIBaseID: base,
    387 					},
    388 					Region: Region{
    389 						StartLine:   pos.Line,
    390 						StartColumn: pos.Column,
    391 					},
    392 				}
    393 			}
    394 			tf = append(tf, tfl)
    395 		}
    396 		tfs = append(tfs, ThreadFlow{Locations: tf})
    397 	}
    398 	return tfs
    399 }
    400 
    401 func fileURIInfo(filename, top, module, version string) (string, string) {
    402 	if top == module {
    403 		return filename, SrcRootID
    404 	}
    405 	if module == internal.GoStdModulePath {
    406 		return filename, GoRootID
    407 	}
    408 	return filepath.ToSlash(filepath.Join(module+"@"+version, filename)), GoModCacheID
    409 }