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 }