|  | // Copyright 2022 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. | 
|  |  | 
|  | //go:build go1.18 | 
|  | // +build go1.18 | 
|  |  | 
|  | // Package vulntest provides helpers for vulncheck functionality testing. | 
|  | package vulntest | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "context" | 
|  | "encoding/json" | 
|  | "fmt" | 
|  | "io/ioutil" | 
|  | "os" | 
|  | "path/filepath" | 
|  | "sort" | 
|  | "strings" | 
|  | "time" | 
|  |  | 
|  | "golang.org/x/tools/gopls/internal/span" | 
|  | "golang.org/x/tools/txtar" | 
|  | "golang.org/x/vuln/client" | 
|  | "golang.org/x/vuln/osv" | 
|  | ) | 
|  |  | 
|  | // NewDatabase returns a read-only DB containing the provided | 
|  | // txtar-format collection of vulnerability reports. | 
|  | // Each vulnerability report is a YAML file whose format | 
|  | // is defined in golang.org/x/vulndb/doc/format.md. | 
|  | // A report file name must have the id as its base name, | 
|  | // and have .yaml as its extension. | 
|  | // | 
|  | //	db, err := NewDatabase(ctx, reports) | 
|  | //	... | 
|  | //	defer db.Clean() | 
|  | //	client, err := NewClient(db) | 
|  | //	... | 
|  | // | 
|  | // The returned DB's Clean method must be called to clean up the | 
|  | // generated database. | 
|  | func NewDatabase(ctx context.Context, txtarReports []byte) (*DB, error) { | 
|  | disk, err := ioutil.TempDir("", "vulndb-test") | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | if err := generateDB(ctx, txtarReports, disk, false); err != nil { | 
|  | os.RemoveAll(disk) | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | return &DB{disk: disk}, nil | 
|  | } | 
|  |  | 
|  | // DB is a read-only vulnerability database on disk. | 
|  | // Users can use this database with golang.org/x/vuln APIs | 
|  | // by setting the `VULNDB“ environment variable. | 
|  | type DB struct { | 
|  | disk string | 
|  | } | 
|  |  | 
|  | // URI returns the file URI that can be used for VULNDB environment | 
|  | // variable. | 
|  | func (db *DB) URI() string { | 
|  | u := span.URIFromPath(db.disk) | 
|  | return string(u) | 
|  | } | 
|  |  | 
|  | // Clean deletes the database. | 
|  | func (db *DB) Clean() error { | 
|  | return os.RemoveAll(db.disk) | 
|  | } | 
|  |  | 
|  | // NewClient returns a vuln DB client that works with the given DB. | 
|  | func NewClient(db *DB) (client.Client, error) { | 
|  | return client.NewClient([]string{db.URI()}, client.Options{}) | 
|  | } | 
|  |  | 
|  | // | 
|  | // The following was selectively copied from golang.org/x/vulndb/internal/database | 
|  | // | 
|  |  | 
|  | const ( | 
|  | dbURL = "https://pkg.go.dev/vuln/" | 
|  |  | 
|  | // idDirectory is the name of the directory that contains entries | 
|  | // listed by their IDs. | 
|  | idDirectory = "ID" | 
|  |  | 
|  | // stdFileName is the name of the .json file in the vulndb repo | 
|  | // that will contain info on standard library vulnerabilities. | 
|  | stdFileName = "stdlib" | 
|  |  | 
|  | // toolchainFileName is the name of the .json file in the vulndb repo | 
|  | // that will contain info on toolchain (cmd/...) vulnerabilities. | 
|  | toolchainFileName = "toolchain" | 
|  |  | 
|  | // cmdModule is the name of the module containing Go toolchain | 
|  | // binaries. | 
|  | cmdModule = "cmd" | 
|  |  | 
|  | // stdModule is the name of the module containing Go std packages. | 
|  | stdModule = "std" | 
|  | ) | 
|  |  | 
|  | // generateDB generates the file-based vuln DB in the directory jsonDir. | 
|  | func generateDB(ctx context.Context, txtarData []byte, jsonDir string, indent bool) error { | 
|  | archive := txtar.Parse(txtarData) | 
|  |  | 
|  | jsonVulns, entries, err := generateEntries(ctx, archive) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | index := make(client.DBIndex, len(jsonVulns)) | 
|  | for modulePath, vulns := range jsonVulns { | 
|  | epath, err := client.EscapeModulePath(modulePath) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | if err := writeVulns(filepath.Join(jsonDir, epath), vulns, indent); err != nil { | 
|  | return err | 
|  | } | 
|  | for _, v := range vulns { | 
|  | if v.Modified.After(index[modulePath]) { | 
|  | index[modulePath] = v.Modified | 
|  | } | 
|  | } | 
|  | } | 
|  | if err := writeJSON(filepath.Join(jsonDir, "index.json"), index, indent); err != nil { | 
|  | return err | 
|  | } | 
|  | if err := writeAliasIndex(jsonDir, entries, indent); err != nil { | 
|  | return err | 
|  | } | 
|  | return writeEntriesByID(filepath.Join(jsonDir, idDirectory), entries, indent) | 
|  | } | 
|  |  | 
|  | func generateEntries(_ context.Context, archive *txtar.Archive) (map[string][]osv.Entry, []osv.Entry, error) { | 
|  | now := time.Now() | 
|  | jsonVulns := map[string][]osv.Entry{} | 
|  | var entries []osv.Entry | 
|  | for _, f := range archive.Files { | 
|  | if !strings.HasSuffix(f.Name, ".yaml") { | 
|  | continue | 
|  | } | 
|  | r, err := readReport(bytes.NewReader(f.Data)) | 
|  | if err != nil { | 
|  | return nil, nil, err | 
|  | } | 
|  | name := strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name)) | 
|  | linkName := fmt.Sprintf("%s%s", dbURL, name) | 
|  | entry, modulePaths := generateOSVEntry(name, linkName, now, *r) | 
|  | for _, modulePath := range modulePaths { | 
|  | jsonVulns[modulePath] = append(jsonVulns[modulePath], entry) | 
|  | } | 
|  | entries = append(entries, entry) | 
|  | } | 
|  | return jsonVulns, entries, nil | 
|  | } | 
|  |  | 
|  | func writeVulns(outPath string, vulns []osv.Entry, indent bool) error { | 
|  | if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { | 
|  | return fmt.Errorf("failed to create directory %q: %s", filepath.Dir(outPath), err) | 
|  | } | 
|  | return writeJSON(outPath+".json", vulns, indent) | 
|  | } | 
|  |  | 
|  | func writeEntriesByID(idDir string, entries []osv.Entry, indent bool) error { | 
|  | // Write a directory containing entries by ID. | 
|  | if err := os.MkdirAll(idDir, 0755); err != nil { | 
|  | return fmt.Errorf("failed to create directory %q: %v", idDir, err) | 
|  | } | 
|  | var idIndex []string | 
|  | for _, e := range entries { | 
|  | outPath := filepath.Join(idDir, e.ID+".json") | 
|  | if err := writeJSON(outPath, e, indent); err != nil { | 
|  | return err | 
|  | } | 
|  | idIndex = append(idIndex, e.ID) | 
|  | } | 
|  | // Write an index.json in the ID directory with a list of all the IDs. | 
|  | return writeJSON(filepath.Join(idDir, "index.json"), idIndex, indent) | 
|  | } | 
|  |  | 
|  | // Write a JSON file containing a map from alias to GO IDs. | 
|  | func writeAliasIndex(dir string, entries []osv.Entry, indent bool) error { | 
|  | aliasToGoIDs := map[string][]string{} | 
|  | for _, e := range entries { | 
|  | for _, a := range e.Aliases { | 
|  | aliasToGoIDs[a] = append(aliasToGoIDs[a], e.ID) | 
|  | } | 
|  | } | 
|  | return writeJSON(filepath.Join(dir, "aliases.json"), aliasToGoIDs, indent) | 
|  | } | 
|  |  | 
|  | func writeJSON(filename string, value any, indent bool) (err error) { | 
|  | j, err := jsonMarshal(value, indent) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | return os.WriteFile(filename, j, 0644) | 
|  | } | 
|  |  | 
|  | func jsonMarshal(v any, indent bool) ([]byte, error) { | 
|  | if indent { | 
|  | return json.MarshalIndent(v, "", "  ") | 
|  | } | 
|  | return json.Marshal(v) | 
|  | } | 
|  |  | 
|  | // generateOSVEntry create an osv.Entry for a report. In addition to the report, it | 
|  | // takes the ID for the vuln and a URL that will point to the entry in the vuln DB. | 
|  | // It returns the osv.Entry and a list of module paths that the vuln affects. | 
|  | func generateOSVEntry(id, url string, lastModified time.Time, r Report) (osv.Entry, []string) { | 
|  | entry := osv.Entry{ | 
|  | ID:        id, | 
|  | Published: r.Published, | 
|  | Modified:  lastModified, | 
|  | Withdrawn: r.Withdrawn, | 
|  | Details:   r.Description, | 
|  | } | 
|  |  | 
|  | moduleMap := make(map[string]bool) | 
|  | for _, m := range r.Modules { | 
|  | switch m.Module { | 
|  | case stdModule: | 
|  | moduleMap[stdFileName] = true | 
|  | case cmdModule: | 
|  | moduleMap[toolchainFileName] = true | 
|  | default: | 
|  | moduleMap[m.Module] = true | 
|  | } | 
|  | entry.Affected = append(entry.Affected, generateAffected(m, url)) | 
|  | } | 
|  | for _, ref := range r.References { | 
|  | entry.References = append(entry.References, osv.Reference{ | 
|  | Type: string(ref.Type), | 
|  | URL:  ref.URL, | 
|  | }) | 
|  | } | 
|  |  | 
|  | var modulePaths []string | 
|  | for module := range moduleMap { | 
|  | modulePaths = append(modulePaths, module) | 
|  | } | 
|  | // TODO: handle missing fields - Aliases | 
|  |  | 
|  | return entry, modulePaths | 
|  | } | 
|  |  | 
|  | func generateAffectedRanges(versions []VersionRange) osv.Affects { | 
|  | a := osv.AffectsRange{Type: osv.TypeSemver} | 
|  | if len(versions) == 0 || versions[0].Introduced == "" { | 
|  | a.Events = append(a.Events, osv.RangeEvent{Introduced: "0"}) | 
|  | } | 
|  | for _, v := range versions { | 
|  | if v.Introduced != "" { | 
|  | a.Events = append(a.Events, osv.RangeEvent{Introduced: v.Introduced.Canonical()}) | 
|  | } | 
|  | if v.Fixed != "" { | 
|  | a.Events = append(a.Events, osv.RangeEvent{Fixed: v.Fixed.Canonical()}) | 
|  | } | 
|  | } | 
|  | return osv.Affects{a} | 
|  | } | 
|  |  | 
|  | func generateImports(m *Module) (imps []osv.EcosystemSpecificImport) { | 
|  | for _, p := range m.Packages { | 
|  | syms := append([]string{}, p.Symbols...) | 
|  | syms = append(syms, p.DerivedSymbols...) | 
|  | sort.Strings(syms) | 
|  | imps = append(imps, osv.EcosystemSpecificImport{ | 
|  | Path:    p.Package, | 
|  | GOOS:    p.GOOS, | 
|  | GOARCH:  p.GOARCH, | 
|  | Symbols: syms, | 
|  | }) | 
|  | } | 
|  | return imps | 
|  | } | 
|  | func generateAffected(m *Module, url string) osv.Affected { | 
|  | name := m.Module | 
|  | switch name { | 
|  | case stdModule: | 
|  | name = "stdlib" | 
|  | case cmdModule: | 
|  | name = "toolchain" | 
|  | } | 
|  | return osv.Affected{ | 
|  | Package: osv.Package{ | 
|  | Name:      name, | 
|  | Ecosystem: osv.GoEcosystem, | 
|  | }, | 
|  | Ranges:           generateAffectedRanges(m.Versions), | 
|  | DatabaseSpecific: osv.DatabaseSpecific{URL: url}, | 
|  | EcosystemSpecific: osv.EcosystemSpecific{ | 
|  | Imports: generateImports(m), | 
|  | }, | 
|  | } | 
|  | } |