| // 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.ExcludedTypes { |
| data(string(er), 2, func(s *summary) int { return s.excludedByType[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 |
| excludedByType map[report.ExcludedType]int |
| regularByReview map[report.ReviewStatus]int |
| } |
| |
| func newSummary() *summary { |
| return &summary{ |
| excludedByType: make(map[report.ExcludedType]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.excludedByType[r.Excluded]++ |
| |
| yearSummary.excluded++ |
| yearSummary.excludedByType[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 |
| } |