// Copyright 2023 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. // Command inspect provides insights into the current contents of vulndb. package main import ( "context" "flag" "fmt" "log" "os" "sort" "strconv" "strings" "text/tabwriter" "time" "golang.org/x/exp/maps" "golang.org/x/vulndb/internal/genericosv" "golang.org/x/vulndb/internal/ghsarepo" "golang.org/x/vulndb/internal/report" "golang.org/x/vulndb/internal/stdlib" ) var ( localGHSA = flag.String("local-ghsa", "", "path to local GHSA repo, instead of cloning remote") detail = flag.Bool("detail", false, "if true, print more details on GHSAs not yet in vulndb") ) func main() { start := time.Now() flag.Parse() ctx := context.Background() rc, err := report.NewLocalClient(ctx, ".") if err != nil { log.Fatal(err) } var gc *ghsarepo.Client if *localGHSA != "" { gc, err = ghsarepo.NewLocalClient(ctx, *localGHSA) if err != nil { log.Fatal(err) } } else { log.Println("cloning remote GHSA repo (use -local-ghsa to speed this up)...") gc, err = ghsarepo.NewDefaultClient() if err != nil { log.Fatal(err) } } overall, byYear := summarize(gc.List(), rc.List()) display(overall, byYear) if *detail { fmt.Println("\n=== GHSAs not yet in vulndb ===") displayGHSAs(overall.ghsasNotInVDB) } fmt.Printf("\n%s\n", time.Since(start).Truncate(time.Millisecond)) } func display(overall *summary, byYear map[int]*summary) { tw := tabwriter.NewWriter(os.Stdout, 2, 4, 2, ' ', 0) years := maps.Keys(byYear) sort.Sort(sort.Reverse(sort.IntSlice(years))) // sort descending headings := func() { fmt.Fprintf(tw, "\t%s", "Total") for _, year := range years { fmt.Fprintf(tw, "\t%4d", year) } fmt.Fprintf(tw, "\n") fmt.Fprintf(tw, "\t%s", "-----") for range years { fmt.Fprintf(tw, "\t%4s", "----") } fmt.Fprintf(tw, "\n") } data := func(desc string, indent int, getData func(s *summary) int) { var indentS string if indent > 0 { indentS = strings.Repeat("--", indent) + " " } fmt.Fprintf(tw, "%s%s\t%4d", indentS, desc, getData(overall)) for _, year := range years { fmt.Fprintf(tw, "\t%4d", getData(byYear[year])) } fmt.Fprintf(tw, "\n") } newline := func() { fmt.Fprintf(tw, "%s\n", strings.Repeat("\t", len(years))) // preserve tab formatting } // Summary of Go reports by year. headings() data("Go reports", 0, func(s *summary) int { return s.reports }) data("Regular reports", 1, func(s *summary) int { return s.regular }) for _, rs := range []report.ReviewStatus{report.Reviewed, report.Unreviewed} { data(rs.String(), 2, func(s *summary) int { return s.regularByReview[rs] }) } data("Withdrawn reports", 1, func(s *summary) int { return s.withdrawn }) data("Excluded reports", 1, func(s *summary) int { return s.excluded }) for _, er := range report.ExcludedReasons { data(string(er), 2, func(s *summary) int { return s.excludedByReason[er] }) } data("Reports with no GHSA (+)", 1, func(s *summary) int { return s.noGHSA }) data("Stdlib, toolchain and x/ reports", 1, func(s *summary) int { return s.firstParty }) // Summary of GHSAs by year. newline() headings() data("GHSAs affecting Go", 0, func(s *summary) int { return s.ghsas }) data("GHSAs not yet in vulndb (++)", 1, func(s *summary) int { return len(s.ghsasNotInVDB) }) // Additional context. newline() fmt.Fprintln(tw, "(+) \"Go reports with no GHSA\" are published third-party Go reports\nwith no corresponding GHSA. (This isn't a problem; it's informational only.)") fmt.Fprintln(tw, "(++) \"GHSAs not yet in vulndb\" are published GHSAs with no corresponding\nGo report. There may already be an open issue on the tracker for these.") tw.Flush() } func displayGHSAs(ghsas []string) { for i, g := range ghsas { fmt.Println() fmt.Printf("%d) %s\n", i+1, g) fmt.Printf("https://github.com/advisories/%s\n", g) fmt.Printf("search issue tracker: https://github.com/golang/vulndb/issues?q=is%%3Aissue+%s\n", g) } } type summary struct { reports, regular, withdrawn, excluded, noGHSA, firstParty int ghsas int ghsasNotInVDB []string excludedByReason map[report.ExcludedReason]int regularByReview map[report.ReviewStatus]int } func newSummary() *summary { return &summary{ excludedByReason: make(map[report.ExcludedReason]int), regularByReview: make(map[report.ReviewStatus]int), } } func summarize(ghsas []*genericosv.Entry, reports []*report.Report) (*summary, map[int]*summary) { overall := newSummary() byYear := make(map[int]*summary) ghsasWithReport := make(map[string]bool) for _, r := range reports { year, err := strconv.Atoi(strings.Split(r.ID, "-")[1]) if err != nil { panic(err) } if _, ok := byYear[year]; !ok { byYear[year] = newSummary() } yearSummary := byYear[year] overall.reports++ yearSummary.reports++ if isFirstParty(r) { overall.firstParty++ yearSummary.firstParty++ } if r.IsExcluded() { overall.excluded++ overall.excludedByReason[r.Excluded]++ yearSummary.excluded++ yearSummary.excludedByReason[r.Excluded]++ } else if r.Withdrawn != nil { overall.withdrawn++ yearSummary.withdrawn++ } else { overall.regular++ yearSummary.regular++ overall.regularByReview[r.ReviewStatus]++ yearSummary.regularByReview[r.ReviewStatus]++ } if len(r.GHSAs) == 0 && r.CVEMetadata == nil { overall.noGHSA++ yearSummary.noGHSA++ } for _, ghsa := range r.GHSAs { ghsasWithReport[ghsa] = true } } for _, ghsa := range ghsas { year := ghsa.Published.Year() if _, ok := byYear[year]; !ok { byYear[year] = newSummary() } yearSummary := byYear[year] overall.ghsas++ yearSummary.ghsas++ if _, ok := ghsasWithReport[ghsa.ID]; !ok { overall.ghsasNotInVDB = append(overall.ghsasNotInVDB, ghsa.ID) yearSummary.ghsasNotInVDB = append(yearSummary.ghsasNotInVDB, ghsa.ID) } } return overall, byYear } func isFirstParty(r *report.Report) bool { for _, m := range r.Modules { if stdlib.IsStdModule(m.Module) || stdlib.IsCmdModule(m.Module) || stdlib.IsXModule(m.Module) { return true } } return false }