golang.org/x/tools/gopls@v0.15.3/internal/vulncheck/vulntest/db.go (about)

     1  // Copyright 2022 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  //go:build go1.18
     6  // +build go1.18
     7  
     8  // Package vulntest provides helpers for vulncheck functionality testing.
     9  package vulntest
    10  
    11  import (
    12  	"bytes"
    13  	"context"
    14  	"encoding/json"
    15  	"fmt"
    16  	"os"
    17  	"path/filepath"
    18  	"sort"
    19  	"strings"
    20  	"time"
    21  
    22  	"golang.org/x/tools/gopls/internal/protocol"
    23  	"golang.org/x/tools/gopls/internal/vulncheck/osv"
    24  	"golang.org/x/tools/txtar"
    25  )
    26  
    27  // NewDatabase returns a read-only DB containing the provided
    28  // txtar-format collection of vulnerability reports.
    29  // Each vulnerability report is a YAML file whose format
    30  // is defined in golang.org/x/vulndb/doc/format.md.
    31  // A report file name must have the id as its base name,
    32  // and have .yaml as its extension.
    33  //
    34  //	db, err := NewDatabase(ctx, reports)
    35  //	...
    36  //	defer db.Clean()
    37  //	client, err := NewClient(db)
    38  //	...
    39  //
    40  // The returned DB's Clean method must be called to clean up the
    41  // generated database.
    42  func NewDatabase(ctx context.Context, txtarReports []byte) (*DB, error) {
    43  	disk, err := os.MkdirTemp("", "vulndb-test")
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	if err := generateDB(ctx, txtarReports, disk, false); err != nil {
    48  		os.RemoveAll(disk)
    49  		return nil, err
    50  	}
    51  
    52  	return &DB{disk: disk}, nil
    53  }
    54  
    55  // DB is a read-only vulnerability database on disk.
    56  // Users can use this database with golang.org/x/vuln APIs
    57  // by setting the `VULNDB“ environment variable.
    58  type DB struct {
    59  	disk string
    60  }
    61  
    62  // URI returns the file URI that can be used for VULNDB environment
    63  // variable.
    64  func (db *DB) URI() string {
    65  	u := protocol.URIFromPath(filepath.Join(db.disk, "ID"))
    66  	return string(u)
    67  }
    68  
    69  // Clean deletes the database.
    70  func (db *DB) Clean() error {
    71  	return os.RemoveAll(db.disk)
    72  }
    73  
    74  //
    75  // The following was selectively copied from golang.org/x/vulndb/internal/database
    76  //
    77  
    78  const (
    79  	dbURL = "https://pkg.go.dev/vuln/"
    80  
    81  	// idDirectory is the name of the directory that contains entries
    82  	// listed by their IDs.
    83  	idDirectory = "ID"
    84  
    85  	// cmdModule is the name of the module containing Go toolchain
    86  	// binaries.
    87  	cmdModule = "cmd"
    88  
    89  	// stdModule is the name of the module containing Go std packages.
    90  	stdModule = "std"
    91  )
    92  
    93  // generateDB generates the file-based vuln DB in the directory jsonDir.
    94  func generateDB(ctx context.Context, txtarData []byte, jsonDir string, indent bool) error {
    95  	archive := txtar.Parse(txtarData)
    96  
    97  	entries, err := generateEntries(ctx, archive)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	return writeEntriesByID(filepath.Join(jsonDir, idDirectory), entries, indent)
   102  }
   103  
   104  func generateEntries(_ context.Context, archive *txtar.Archive) ([]osv.Entry, error) {
   105  	now := time.Now()
   106  	var entries []osv.Entry
   107  	for _, f := range archive.Files {
   108  		if !strings.HasSuffix(f.Name, ".yaml") {
   109  			continue
   110  		}
   111  		r, err := readReport(bytes.NewReader(f.Data))
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  		name := strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name))
   116  		linkName := fmt.Sprintf("%s%s", dbURL, name)
   117  		entry := generateOSVEntry(name, linkName, now, *r)
   118  		entries = append(entries, entry)
   119  	}
   120  	return entries, nil
   121  }
   122  
   123  func writeEntriesByID(idDir string, entries []osv.Entry, indent bool) error {
   124  	// Write a directory containing entries by ID.
   125  	if err := os.MkdirAll(idDir, 0755); err != nil {
   126  		return fmt.Errorf("failed to create directory %q: %v", idDir, err)
   127  	}
   128  	for _, e := range entries {
   129  		outPath := filepath.Join(idDir, e.ID+".json")
   130  		if err := writeJSON(outPath, e, indent); err != nil {
   131  			return err
   132  		}
   133  	}
   134  	return nil
   135  }
   136  
   137  func writeJSON(filename string, value any, indent bool) (err error) {
   138  	j, err := jsonMarshal(value, indent)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	return os.WriteFile(filename, j, 0644)
   143  }
   144  
   145  func jsonMarshal(v any, indent bool) ([]byte, error) {
   146  	if indent {
   147  		return json.MarshalIndent(v, "", "  ")
   148  	}
   149  	return json.Marshal(v)
   150  }
   151  
   152  // generateOSVEntry create an osv.Entry for a report. In addition to the report, it
   153  // takes the ID for the vuln and a URL that will point to the entry in the vuln DB.
   154  // It returns the osv.Entry and a list of module paths that the vuln affects.
   155  func generateOSVEntry(id, url string, lastModified time.Time, r Report) osv.Entry {
   156  	entry := osv.Entry{
   157  		ID:               id,
   158  		Published:        r.Published,
   159  		Modified:         lastModified,
   160  		Withdrawn:        r.Withdrawn,
   161  		Summary:          r.Summary,
   162  		Details:          r.Description,
   163  		DatabaseSpecific: &osv.DatabaseSpecific{URL: url},
   164  	}
   165  
   166  	moduleMap := make(map[string]bool)
   167  	for _, m := range r.Modules {
   168  		switch m.Module {
   169  		case stdModule:
   170  			moduleMap[osv.GoStdModulePath] = true
   171  		case cmdModule:
   172  			moduleMap[osv.GoCmdModulePath] = true
   173  		default:
   174  			moduleMap[m.Module] = true
   175  		}
   176  		entry.Affected = append(entry.Affected, toAffected(m))
   177  	}
   178  	for _, ref := range r.References {
   179  		entry.References = append(entry.References, osv.Reference{
   180  			Type: ref.Type,
   181  			URL:  ref.URL,
   182  		})
   183  	}
   184  	return entry
   185  }
   186  
   187  func AffectedRanges(versions []VersionRange) []osv.Range {
   188  	a := osv.Range{Type: osv.RangeTypeSemver}
   189  	if len(versions) == 0 || versions[0].Introduced == "" {
   190  		a.Events = append(a.Events, osv.RangeEvent{Introduced: "0"})
   191  	}
   192  	for _, v := range versions {
   193  		if v.Introduced != "" {
   194  			a.Events = append(a.Events, osv.RangeEvent{Introduced: v.Introduced.Canonical()})
   195  		}
   196  		if v.Fixed != "" {
   197  			a.Events = append(a.Events, osv.RangeEvent{Fixed: v.Fixed.Canonical()})
   198  		}
   199  	}
   200  	return []osv.Range{a}
   201  }
   202  
   203  func toOSVPackages(pkgs []*Package) (imps []osv.Package) {
   204  	for _, p := range pkgs {
   205  		syms := append([]string{}, p.Symbols...)
   206  		syms = append(syms, p.DerivedSymbols...)
   207  		sort.Strings(syms)
   208  		imps = append(imps, osv.Package{
   209  			Path:    p.Package,
   210  			GOOS:    p.GOOS,
   211  			GOARCH:  p.GOARCH,
   212  			Symbols: syms,
   213  		})
   214  	}
   215  	return imps
   216  }
   217  
   218  func toAffected(m *Module) osv.Affected {
   219  	name := m.Module
   220  	switch name {
   221  	case stdModule:
   222  		name = osv.GoStdModulePath
   223  	case cmdModule:
   224  		name = osv.GoCmdModulePath
   225  	}
   226  	return osv.Affected{
   227  		Module: osv.Module{
   228  			Path:      name,
   229  			Ecosystem: osv.GoEcosystem,
   230  		},
   231  		Ranges: AffectedRanges(m.Versions),
   232  		EcosystemSpecific: osv.EcosystemSpecific{
   233  			Packages: toOSVPackages(m.Packages),
   234  		},
   235  	}
   236  }