src

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

handler.go (6253B)


      1 // Copyright 2024 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 openvex
      6 
      7 import (
      8 	"crypto/sha256"
      9 	"encoding/json"
     10 	"fmt"
     11 	"io"
     12 	"slices"
     13 	"time"
     14 
     15 	"golang.org/x/vuln/internal/govulncheck"
     16 	"golang.org/x/vuln/internal/osv"
     17 )
     18 
     19 type findingLevel int
     20 
     21 const (
     22 	invalid findingLevel = iota
     23 	required
     24 	imported
     25 	called
     26 )
     27 
     28 type handler struct {
     29 	w    io.Writer
     30 	cfg  *govulncheck.Config
     31 	sbom *govulncheck.SBOM
     32 	osvs map[string]*osv.Entry
     33 	// findings contains same-level findings for an
     34 	// OSV at the most precise level of granularity
     35 	// available. This means, for instance, that if
     36 	// an osv is indeed called, then all findings for
     37 	// the osv will have call stack info.
     38 	findings map[string][]*govulncheck.Finding
     39 }
     40 
     41 func NewHandler(w io.Writer) *handler {
     42 	return &handler{
     43 		w:        w,
     44 		osvs:     make(map[string]*osv.Entry),
     45 		findings: make(map[string][]*govulncheck.Finding),
     46 	}
     47 }
     48 
     49 func (h *handler) Config(cfg *govulncheck.Config) error {
     50 	h.cfg = cfg
     51 	return nil
     52 }
     53 
     54 func (h *handler) Progress(progress *govulncheck.Progress) error {
     55 	return nil
     56 }
     57 
     58 func (h *handler) SBOM(s *govulncheck.SBOM) error {
     59 	h.sbom = s
     60 	return nil
     61 }
     62 
     63 func (h *handler) OSV(e *osv.Entry) error {
     64 	h.osvs[e.ID] = e
     65 	return nil
     66 }
     67 
     68 // foundAtLevel returns the level at which a specific finding is present in the
     69 // scanned product.
     70 func foundAtLevel(f *govulncheck.Finding) findingLevel {
     71 	frame := f.Trace[0]
     72 	if frame.Function != "" {
     73 		return called
     74 	}
     75 	if frame.Package != "" {
     76 		return imported
     77 	}
     78 	return required
     79 }
     80 
     81 // moreSpecific favors a call finding over a non-call
     82 // finding and a package finding over a module finding.
     83 func moreSpecific(f1, f2 *govulncheck.Finding) int {
     84 	if len(f1.Trace) > 1 && len(f2.Trace) > 1 {
     85 		// Both are call stack findings.
     86 		return 0
     87 	}
     88 	if len(f1.Trace) > 1 {
     89 		return -1
     90 	}
     91 	if len(f2.Trace) > 1 {
     92 		return 1
     93 	}
     94 
     95 	fr1, fr2 := f1.Trace[0], f2.Trace[0]
     96 	if fr1.Function != "" && fr2.Function == "" {
     97 		return -1
     98 	}
     99 	if fr1.Function == "" && fr2.Function != "" {
    100 		return 1
    101 	}
    102 	if fr1.Package != "" && fr2.Package == "" {
    103 		return -1
    104 	}
    105 	if fr1.Package == "" && fr2.Package != "" {
    106 		return -1
    107 	}
    108 	return 0 // findings always have module info
    109 }
    110 
    111 func (h *handler) Finding(f *govulncheck.Finding) error {
    112 	fs := h.findings[f.OSV]
    113 	if len(fs) == 0 {
    114 		fs = []*govulncheck.Finding{f}
    115 	} else {
    116 		if ms := moreSpecific(f, fs[0]); ms == -1 {
    117 			// The new finding is more specific, so we need
    118 			// to erase existing findings and add the new one.
    119 			fs = []*govulncheck.Finding{f}
    120 		} else if ms == 0 {
    121 			// The new finding is at the same level of precision.
    122 			fs = append(fs, f)
    123 		}
    124 		// Otherwise, the new finding is at a less precise level.
    125 	}
    126 	h.findings[f.OSV] = fs
    127 	return nil
    128 }
    129 
    130 // Flush is used to print the vex json to w.
    131 // This is needed as vex is not streamed.
    132 func (h *handler) Flush() error {
    133 	doc := toVex(h)
    134 	out, err := json.MarshalIndent(doc, "", "  ")
    135 	if err != nil {
    136 		return err
    137 	}
    138 	_, err = h.w.Write(out)
    139 	return err
    140 }
    141 
    142 func toVex(h *handler) Document {
    143 	doc := Document{
    144 		Context:    ContextURI,
    145 		Author:     DefaultAuthor,
    146 		Timestamp:  time.Now().UTC(),
    147 		Version:    1,
    148 		Tooling:    Tooling,
    149 		Statements: statements(h),
    150 	}
    151 
    152 	id := hashVex(doc)
    153 	doc.ID = "govulncheck/vex:" + id
    154 	return doc
    155 }
    156 
    157 // Given a slice of findings, returns those findings as a set of subcomponents
    158 // that are unique per the vulnerable artifact's PURL.
    159 func subcomponentSet(findings []*govulncheck.Finding) []Component {
    160 	var scs []Component
    161 	seen := make(map[string]bool)
    162 	for _, f := range findings {
    163 		purl := purlFromFinding(f)
    164 		if !seen[purl] {
    165 			scs = append(scs, Component{
    166 				ID: purlFromFinding(f),
    167 			})
    168 			seen[purl] = true
    169 		}
    170 	}
    171 	return scs
    172 }
    173 
    174 // statements combines all OSVs found by govulncheck and generates the list of
    175 // vex statements with the proper affected level and justification to match the
    176 // openVex specification.
    177 func statements(h *handler) []Statement {
    178 	var scanLevel findingLevel
    179 	switch h.cfg.ScanLevel {
    180 	case govulncheck.ScanLevelModule:
    181 		scanLevel = required
    182 	case govulncheck.ScanLevelPackage:
    183 		scanLevel = imported
    184 	case govulncheck.ScanLevelSymbol:
    185 		scanLevel = called
    186 	}
    187 
    188 	var statements []Statement
    189 	for id, osv := range h.osvs {
    190 		// if there are no findings emitted for a given OSV that means that
    191 		// the vulnerable module is not required at a vulnerable version.
    192 		if len(h.findings[id]) == 0 {
    193 			continue
    194 		}
    195 		description := osv.Summary
    196 		if description == "" {
    197 			description = osv.Details
    198 		}
    199 
    200 		s := Statement{
    201 			Vulnerability: Vulnerability{
    202 				ID:          fmt.Sprintf("https://pkg.go.dev/vuln/%s", id),
    203 				Name:        id,
    204 				Description: description,
    205 				Aliases:     osv.Aliases,
    206 			},
    207 			Products: []Product{
    208 				{
    209 					Component:     Component{ID: DefaultPID},
    210 					Subcomponents: subcomponentSet(h.findings[id]),
    211 				},
    212 			},
    213 		}
    214 
    215 		// Findings are guaranteed to be at the same level, so we can just check the first element
    216 		fLevel := foundAtLevel(h.findings[id][0])
    217 		if fLevel >= scanLevel {
    218 			s.Status = StatusAffected
    219 		} else {
    220 			s.Status = StatusNotAffected
    221 			s.ImpactStatement = Impact
    222 			s.Justification = JustificationNotPresent
    223 			// We only reach this case if running in symbol mode
    224 			if fLevel == imported {
    225 				s.Justification = JustificationNotExecuted
    226 			}
    227 		}
    228 		statements = append(statements, s)
    229 	}
    230 
    231 	slices.SortFunc(statements, func(a, b Statement) int {
    232 		if a.Vulnerability.ID > b.Vulnerability.ID {
    233 			return 1
    234 		}
    235 		if a.Vulnerability.ID < b.Vulnerability.ID {
    236 			return -1
    237 		}
    238 		// this should never happen in practice, since statements are being
    239 		// populated from a map with the vulnerability IDs as keys
    240 		return 0
    241 	})
    242 	return statements
    243 }
    244 
    245 func hashVex(doc Document) string {
    246 	// json.Marshal should never error here (because of the structure of Document).
    247 	// If an error does occur, it won't be a jsonerror, but instead a panic
    248 	d := Document{
    249 		Context:    doc.Context,
    250 		ID:         doc.ID,
    251 		Author:     doc.Author,
    252 		Version:    doc.Version,
    253 		Tooling:    doc.Tooling,
    254 		Statements: doc.Statements,
    255 	}
    256 	out, err := json.Marshal(d)
    257 	if err != nil {
    258 		panic(err)
    259 	}
    260 	return fmt.Sprintf("%x", sha256.Sum256(out))
    261 }