| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package openvex |
| |
| import ( |
| "crypto/sha256" |
| "encoding/json" |
| "fmt" |
| "io" |
| "slices" |
| "time" |
| |
| "golang.org/x/vuln/internal/govulncheck" |
| "golang.org/x/vuln/internal/osv" |
| ) |
| |
| type findingLevel int |
| |
| const ( |
| invalid findingLevel = iota |
| required |
| imported |
| called |
| ) |
| |
| type handler struct { |
| w io.Writer |
| cfg *govulncheck.Config |
| sbom *govulncheck.SBOM |
| osvs map[string]*osv.Entry |
| // findings contains same-level findings for an |
| // OSV at the most precise level of granularity |
| // available. This means, for instance, that if |
| // an osv is indeed called, then all findings for |
| // the osv will have call stack info. |
| findings map[string][]*govulncheck.Finding |
| } |
| |
| func NewHandler(w io.Writer) *handler { |
| return &handler{ |
| w: w, |
| osvs: make(map[string]*osv.Entry), |
| findings: make(map[string][]*govulncheck.Finding), |
| } |
| } |
| |
| func (h *handler) Config(cfg *govulncheck.Config) error { |
| h.cfg = cfg |
| return nil |
| } |
| |
| func (h *handler) Progress(progress *govulncheck.Progress) error { |
| return nil |
| } |
| |
| func (h *handler) SBOM(s *govulncheck.SBOM) error { |
| h.sbom = s |
| return nil |
| } |
| |
| func (h *handler) OSV(e *osv.Entry) error { |
| h.osvs[e.ID] = e |
| return nil |
| } |
| |
| // foundAtLevel returns the level at which a specific finding is present in the |
| // scanned product. |
| func foundAtLevel(f *govulncheck.Finding) findingLevel { |
| frame := f.Trace[0] |
| if frame.Function != "" { |
| return called |
| } |
| if frame.Package != "" { |
| return imported |
| } |
| return required |
| } |
| |
| // moreSpecific favors a call finding over a non-call |
| // finding and a package finding over a module finding. |
| func moreSpecific(f1, f2 *govulncheck.Finding) int { |
| if len(f1.Trace) > 1 && len(f2.Trace) > 1 { |
| // Both are call stack findings. |
| return 0 |
| } |
| if len(f1.Trace) > 1 { |
| return -1 |
| } |
| if len(f2.Trace) > 1 { |
| return 1 |
| } |
| |
| fr1, fr2 := f1.Trace[0], f2.Trace[0] |
| if fr1.Function != "" && fr2.Function == "" { |
| return -1 |
| } |
| if fr1.Function == "" && fr2.Function != "" { |
| return 1 |
| } |
| if fr1.Package != "" && fr2.Package == "" { |
| return -1 |
| } |
| if fr1.Package == "" && fr2.Package != "" { |
| return -1 |
| } |
| return 0 // findings always have module info |
| } |
| |
| func (h *handler) Finding(f *govulncheck.Finding) error { |
| fs := h.findings[f.OSV] |
| if len(fs) == 0 { |
| fs = []*govulncheck.Finding{f} |
| } else { |
| if ms := moreSpecific(f, fs[0]); ms == -1 { |
| // The new finding is more specific, so we need |
| // to erase existing findings and add the new one. |
| fs = []*govulncheck.Finding{f} |
| } else if ms == 0 { |
| // The new finding is at the same level of precision. |
| fs = append(fs, f) |
| } |
| // Otherwise, the new finding is at a less precise level. |
| } |
| h.findings[f.OSV] = fs |
| return nil |
| } |
| |
| // Flush is used to print the vex json to w. |
| // This is needed as vex is not streamed. |
| func (h *handler) Flush() error { |
| doc := toVex(h) |
| out, err := json.MarshalIndent(doc, "", " ") |
| if err != nil { |
| return err |
| } |
| _, err = h.w.Write(out) |
| return err |
| } |
| |
| func toVex(h *handler) Document { |
| doc := Document{ |
| Context: ContextURI, |
| Author: DefaultAuthor, |
| Timestamp: time.Now().UTC(), |
| Version: 1, |
| Tooling: Tooling, |
| Statements: statements(h), |
| } |
| |
| id := hashVex(doc) |
| doc.ID = "govulncheck/vex:" + id |
| return doc |
| } |
| |
| // Given a slice of findings, returns those findings as a set of subcomponents |
| // that are unique per the vulnerable artifact's PURL. |
| func subcomponentSet(findings []*govulncheck.Finding) []Component { |
| var scs []Component |
| seen := make(map[string]bool) |
| for _, f := range findings { |
| purl := purlFromFinding(f) |
| if !seen[purl] { |
| scs = append(scs, Component{ |
| ID: purlFromFinding(f), |
| }) |
| seen[purl] = true |
| } |
| } |
| return scs |
| } |
| |
| // statements combines all OSVs found by govulncheck and generates the list of |
| // vex statements with the proper affected level and justification to match the |
| // openVex specification. |
| func statements(h *handler) []Statement { |
| var scanLevel findingLevel |
| switch h.cfg.ScanLevel { |
| case govulncheck.ScanLevelModule: |
| scanLevel = required |
| case govulncheck.ScanLevelPackage: |
| scanLevel = imported |
| case govulncheck.ScanLevelSymbol: |
| scanLevel = called |
| } |
| |
| var statements []Statement |
| for id, osv := range h.osvs { |
| // if there are no findings emitted for a given OSV that means that |
| // the vulnerable module is not required at a vulnerable version. |
| if len(h.findings[id]) == 0 { |
| continue |
| } |
| description := osv.Summary |
| if description == "" { |
| description = osv.Details |
| } |
| |
| s := Statement{ |
| Vulnerability: Vulnerability{ |
| ID: fmt.Sprintf("https://pkg.go.dev/vuln/%s", id), |
| Name: id, |
| Description: description, |
| Aliases: osv.Aliases, |
| }, |
| Products: []Product{ |
| { |
| Component: Component{ID: DefaultPID}, |
| Subcomponents: subcomponentSet(h.findings[id]), |
| }, |
| }, |
| } |
| |
| // Findings are guaranteed to be at the same level, so we can just check the first element |
| fLevel := foundAtLevel(h.findings[id][0]) |
| if fLevel >= scanLevel { |
| s.Status = StatusAffected |
| } else { |
| s.Status = StatusNotAffected |
| s.ImpactStatement = Impact |
| s.Justification = JustificationNotPresent |
| // We only reach this case if running in symbol mode |
| if fLevel == imported { |
| s.Justification = JustificationNotExecuted |
| } |
| } |
| statements = append(statements, s) |
| } |
| |
| slices.SortFunc(statements, func(a, b Statement) int { |
| if a.Vulnerability.ID > b.Vulnerability.ID { |
| return 1 |
| } |
| if a.Vulnerability.ID < b.Vulnerability.ID { |
| return -1 |
| } |
| // this should never happen in practice, since statements are being |
| // populated from a map with the vulnerability IDs as keys |
| return 0 |
| }) |
| return statements |
| } |
| |
| func hashVex(doc Document) string { |
| // json.Marshal should never error here (because of the structure of Document). |
| // If an error does occur, it won't be a jsonerror, but instead a panic |
| d := Document{ |
| Context: doc.Context, |
| ID: doc.ID, |
| Author: doc.Author, |
| Version: doc.Version, |
| Tooling: doc.Tooling, |
| Statements: doc.Statements, |
| } |
| out, err := json.Marshal(d) |
| if err != nil { |
| panic(err) |
| } |
| return fmt.Sprintf("%x", sha256.Sum256(out)) |
| } |